Skip to content

Latest commit

 

History

History
122 lines (79 loc) · 9.25 KB

File metadata and controls

122 lines (79 loc) · 9.25 KB

Architecture

This repository defines a golden path for web/mobile products: shared contracts, one backend, two app clients, a static landing project, and little custom infrastructure.

Contracts

packages/contracts is the source of truth for API payloads, DTOs, and error shapes. New endpoints should start with Zod schemas in contracts. The backend then uses those schemas for request validation, while web and mobile use them in TanStack Form and API clients.

Do not hand-copy API shapes into clients. When a contract changes, validate producer and consumers in one pass: backend route/service, web API client/form, and mobile API client/form.

Backend

Backend API code follows this flow:

Hono route -> Zod validation -> auth/session guard -> feature service -> Prisma -> DTO
  • src/index.ts is the API runtime entrypoint.
  • src/worker.ts is the long-running worker entrypoint. Keep it disabled in deployment specs until a real background handler is registered.
  • src/cron.ts is the one-shot scheduled-job entrypoint. Add concrete tasks to its registry and deploy scheduled jobs only for named product tasks.
  • src/runtime.ts owns shared env loading, Prisma creation, and runtime cleanup for all backend entrypoints.
  • src/app.ts owns the Hono app, CORS, secure headers, error handling, route mounting, and OpenAPI output.
  • src/env.ts validates environment variables with Zod.
  • src/db.ts creates the Prisma client.
  • src/auth/* owns the auth feature: routes, service logic, JWT helpers, password hashing, and refresh-token hashing.

Routes should stay thin. Do not put business logic into Hono handlers, UI clients, or child components when the decision belongs in a backend service.

Runtime Shape And Real-Time

The default runtime shape is a modular monolith: one backend codebase, one database, shared contracts, and clear feature boundaries inside the repository. The backend can expose separate API, worker, and cron entrypoints while still sharing Prisma schema, env validation, services, and contracts. Do not add queues, brokers, or extra infrastructure until the product has a concrete need that the monolith cannot meet clearly.

On the default DigitalOcean production path, run the backend/API as one apps-s-1vcpu-1gb App Platform container so the starting infrastructure stays inside the low-cost budget when paired with the smallest production Managed PostgreSQL cluster. Add App Platform worker or scheduled-job components from the same backend/Dockerfile only when the product has a concrete background or periodic task. web and landing remain App Platform Static Site components and do not have runtime container sizes.

For real-time features such as chat, presence, collaboration, live notifications, or activity feeds, start with the same backend service. A single instance can keep an in-memory registry of its own WebSocket connections. Once the backend runs multiple instances, in-memory fanout is no longer enough: one user may be connected to instance A while another is connected to instance B. At that point, add a managed Redis-compatible Pub/Sub broker between backend instances so each instance can publish domain events and subscribe to events it must deliver to its local sockets.

On the default DigitalOcean path, use DigitalOcean Managed Valkey for this broker. On the optional Yandex Cloud path, use Yandex Managed Service for Valkey. Add this infrastructure only when horizontal scaling and cross-instance WebSocket/SSE delivery are actually required; it is not part of the baseline local setup.

Valkey Pub/Sub is only a fanout mechanism. Keep durable chat messages, notifications, collaboration state, and audit-relevant events in PostgreSQL; publish compact event identifiers after commits; and make clients recover by reconnecting and refetching from the API after missed realtime messages.

Auth

Auth v1 is custom JWT-based auth:

  • Passwords use Bun.password.hash/verify with Argon2id.
  • Access tokens are short-lived JWTs signed and verified with jose.
  • Refresh tokens are opaque random tokens; only their SHA-256 hash is stored in PostgreSQL.
  • Web keeps the refresh token in an HttpOnly cookie and keeps the access token in memory. Local HTTP uses SameSite=Lax; HTTPS production uses Secure and SameSite=None so browser auth works across separate web/API origins.
  • Mobile keeps the refresh token in expo-secure-store and keeps the access token in memory.

Refresh-token rotation creates a new session and revokes the previous one. /api/auth/me checks both the JWT and the active database session.

Frontend

Web and mobile follow the same client rules:

  • TanStack Query owns server state.
  • TanStack Form owns form state.
  • Zod schemas come from @web-app-demo/contracts.
  • The API client centralizes base URL handling, auth headers, refresh/retry behavior, and error shape parsing.

Do not create a new form, query, auth, or API abstraction until the existing pattern stops solving the current problem.

landing is a separate Astro workspace for a static landing page. It does not own the auth flow and should not duplicate the browser client from web. If the landing project starts reading API data or shared DTOs, connect @web-app-demo/contracts and validate producer/consumer sides the same way as web and mobile.

Testing

Backend unit/integration tests verify contracts and auth behavior at the owning layer. Web E2E uses Playwright and starts a real backend + Vite through webServer. Mobile E2E uses Maestro and stable React Native testID selectors.

Client E2E in this template is a happy-path smoke layer, not the place for large validation matrices. Keep negative payloads, password/JWT/session rules, and error-shape checks in backend tests. Add fast client-level tests for form validation and API state edge cases when those surfaces grow.

Prisma

Do not hand-write Prisma migration SQL. Change backend/prisma/schema.prisma, then use:

bun run --cwd backend prisma:migrate

The template uses database-generated UUIDv7 primary keys (`@default(dbgenerated("uuidv7()")) @db.Uuid`) instead of ORM-generated `cuid()`/`uuid()`. That keeps ID generation consistent for Prisma Client, direct SQL, imports, and any future background workers or non-Prisma writers, but it also means the schema requires PostgreSQL 18+.

Treat UUIDv7 as a repository-level rule, not a one-off model detail. New primary keys should use database-generated UUIDv7, and foreign keys that reference those IDs should use `@db.Uuid` so the type stays native all the way through PostgreSQL and Prisma.

For production, apply already-created migrations:

bun run --cwd backend prisma:deploy

Local Infrastructure

Local PostgreSQL is provided by Docker Compose, not by a native database install. The development service uses postgres:18-alpine, exposes web_app_demo on host port 54329, and stores data in the postgres_18_data volume. The test service uses the same image with database web_app_demo_test; automated runners set POSTGRES_TEST_PORT to a repository-derived port when they need isolation. PostgreSQL 18 is intentional here because the backend schema relies on the native uuidv7() database function.

Keep docker-compose.yml, backend/.env.example, .env.example, and LOCAL_DATABASE.md aligned when changing local database names, ports, credentials, image tags, or volume paths.

Storage

Persistent files and media belong in DigitalOcean Spaces, not in the App Platform container filesystem. The backend owns storage access through src/storage, including safe object keys, presigned uploads/downloads, public CDN URL construction, and object deletion. Product features that use uploads should store ownership and retention metadata in PostgreSQL when permissions, deletion, audit, or private access matter.

For image optimization, generate app-owned variants in the backend, a worker, or a dedicated App Platform service, then store those variants in Spaces and serve public variants through Spaces CDN. DigitalOcean Spaces and Spaces CDN do not provide first-party dynamic image resizing or format transformation.

Current Upstream Documentation

For framework and API questions, consult the current upstream documentation linked here first. This document describes repository conventions; upstream docs are authoritative for tool behavior.