Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save SomeoneWeird/ad95acdb8fc3b02ee7d6ea7b2d3d6c4e to your computer and use it in GitHub Desktop.

Select an option

Save SomeoneWeird/ad95acdb8fc3b02ee7d6ea7b2d3d6c4e to your computer and use it in GitHub Desktop.
Accession v1 Next.js application architecture

Accession v1 application architecture

Decision

Build Accession as a Vercel-first, application-portable Next.js modular monolith. Use Node.js as the default runtime, Edge only for a measured route-specific benefit, and Vercel Workflow for durable background work. Do not make the application Edge-only.

Repository and module boundaries

  • Use a pnpm 10 workspace orchestrated by Turborepo 2.9.
  • Keep one deployable application in apps/web; Collector and Catalog Administrator experiences are protected route groups in that application.
  • Give each capability its own private workspace package under features/: identity, catalog, purchasing, inventory, wishlist, import-export, and curation.
  • Put shared infrastructure under packages/: db, auth, storage, workflow, observability, ui, and config. Put repository tooling under tooling/.
  • Keep apps/web/app as delivery and composition wiring. Features expose deliberate command, query, contract, UI, and Adapter entry points; deep imports and root wildcard barrels are forbidden.
  • Feature-specific UI stays with its feature. packages/ui owns only source-controlled shadcn primitives and design tokens.
  • Keep Drizzle schemas and persistence Adapters with the owning feature. packages/db supplies connections, Collector-scoped transaction/RLS context, and migration infrastructure; it is not a generic repository layer.
  • Cross-feature dependencies must be explicit and acyclic. Do not create global types, utils, or one catch-all feature package.
  • Do not introduce microservices, microfrontends, GraphQL, tRPC, or an independent worker application in v1.

Framework and user interface

  • Use the Next.js 16 Active LTS line, React 19.2, Node.js 24 Active LTS, TypeScript 6, Turbopack, and the App Router.
  • Enable React Compiler. Leave Cache Components off initially; any caching of Collector-private data must be explicit and reviewed.
  • Use Tailwind CSS v4 and source-owned shadcn/ui CLI v4 components on Base UI primitives, with semantic OKLCH CSS-variable tokens, Lucide icons, and next-themes.
  • Use native forms, Server Actions, React useActionState, and Zod 4 validation. Server validation and Module authorization remain authoritative.
  • Put filter, sort, and pagination state in the URL; use local React state only for transient interaction. Do not add React Hook Form, TanStack Query, Zustand, or Redux by default. React Hook Form may be introduced later for a demonstrably complex dynamic form.
  • Use shadcn Table for simple tables and TanStack Table v8 for dense inventory, purchase, catalog, and curation views. PostgreSQL performs sorting, filtering, and pagination. Add TanStack Virtual only after profiling proves it necessary. Mobile layouts use purpose-built lists/cards rather than compressed desktop grids.

Database and Money

  • PostgreSQL is the sole system of record. Use Neon through the Vercel Marketplace in AWS Sydney, colocated with the primary Vercel Node compute.
  • Use Drizzle ORM with generated, reviewed, committed, forward-only SQL migrations. Production never uses drizzle-kit push.
  • Use pooled runtime connections for application transactions and a direct controlled connection for migrations.
  • Use normalized relational tables for domain state, append-only revision/audit tables for history, and JSONB only for immutable provider, import, and evidence shapes.
  • Enforce Collector scoping in every Module Interface and with default-deny PostgreSQL row-level security.
  • Represent Money as an immutable value containing a decimal coefficient held as bigint, an explicit scale, uppercase currency code, and Currency Definition revision. Never use JavaScript floating point or a normalized generic decimal value as its authoritative form.

Authentication and privacy

  • Use Better Auth with its Drizzle Adapter and passkey plugin. Authentication and sensitive-operation reauthentication are Passkey-only.
  • Registration asks for email first, normalizes it, reserves it uniquely when the Collector is created, and then registers the first Passkey.
  • Email verification is deferred and does not block use. Before verification, email is only a Collector-supplied private claim and receives nothing except a future verification message.
  • Email is never a sign-in identifier or lost-Passkey recovery method. Duplicate registration errors are generic; email correction requires fresh Passkey authentication.
  • A Collector may register multiple Passkeys. Sessions use the previously resolved 30-day inactivity and 90-day absolute limits, local/everywhere logout, and fresh Passkey checks for sensitive operations.
  • This explicitly supersedes the earlier email-free clause in the Collector-tenancy decision; all other strict Collector-scoping and privacy conclusions remain in force.

Assets, files, and images

  • Use Vercel Private Blob behind a storage Interface, with separate Catalog Asset, Collector Asset, and Transient Job File boundaries.
  • Blob paths are opaque and immutable. PostgreSQL owns asset identity, provenance, rights, authorization, lifecycle, and availability state.
  • Catalog Assets are served only while the current publication and rights policy permits it. Collector Assets require server-side Collector authorization. Import uploads and generated exports are lifecycle-deleted transient files.
  • Never render third-party image URLs or permanently public asset URLs in the browser.
  • Use Sharp in Node workflow steps to validate image signatures and limits, normalize orientation, remove metadata, and generate derivatives. Raw uploads are transient; only sanitized canonical assets and derivatives enter durable private storage.

Durable work and email

  • Use Vercel Workflow plus a PostgreSQL transactional outbox; do not add Redis or another queue in v1.
  • The domain transaction writes an Outbox Message. A post-commit dispatcher starts the workflow, and a scheduled recovery sweep retries undispatched messages.
  • Workflow inputs contain opaque identifiers only. Idempotent steps reload authoritative state from PostgreSQL/Blob and enforce expected revisions.
  • Human confirmation state lives in PostgreSQL. A preview workflow ends at the confirmation boundary; confirmation starts a new workflow rather than leaving a durable execution waiting indefinitely.
  • Select Resend through a feature-owned Adapter with React Email templates for future verification delivery. Sends originate from the outbox/workflow and use deterministic idempotency keys. Verification delivery and enforcement remain deferred from v1.

Environments and release

  • Keep Local, Preview, and Production isolated: no shared databases, Blob stores, Workflow state, secrets, sessions, or Passkeys.
  • Local development uses Docker Compose for PostgreSQL, an S3-compatible storage emulator behind the storage Adapter, and Mailpit. Better Auth and the local Workflow runtime execute with the application. Do not add Redis.
  • Protected Vercel previews use per-branch Neon databases derived from a sanitized non-production template, dedicated non-production Blob/Workflow resources, and a stable branch hostname for WebAuthn.
  • Production uses separate Neon, Blob, Workflow, Better Auth secrets, and Resend configuration.
  • Release promotion uses the exact tested artifact. Forward-only migrations, migration compatibility, integration tests, and a production build are release gates.

Tooling and verification

  • Pin the exact pnpm version, use pnpm catalogs, and apply a seven-day minimum dependency age with reviewed exceptions.
  • Use Oxfmt for formatting, import ordering, package.json ordering, and Tailwind v4 class sorting.
  • Use Oxlint with native TypeScript, React, Next.js, JSX accessibility, import, Vitest, and Oxc rules; enforce react/react-compiler as an error. Use fast linting during development and exactly pinned oxlint-tsgolint for type-aware CI linting.
  • Use an exactly pinned tsgo for fast local type-checking. next build remains the authoritative TypeScript and framework gate.
  • Do not add ESLint, Prettier, Biome, Ultracite, Husky, or mandatory pre-commit hooks.
  • Use Vitest 4 for domain, application, Adapter, and workflow tests; Vitest Browser Mode with the Playwright provider for interactive client components; and Playwright Test for complete journeys.
  • Exercise real passkey registration and authentication through Playwright's virtual WebAuthn authenticator. Better Auth test utilities may seed an authenticated session only when the test is not about the ceremony.
  • Integration tests run real committed migrations against PostgreSQL and use the real local storage and Workflow services where relevant. Mock only external-provider Interfaces.
  • Use @axe-core/playwright on critical journeys plus manual keyboard and screen-reader acceptance. Concentrate coverage requirements on domain invariants and failures rather than a repository-wide vanity percentage.
  • Pull requests run Turborepo affected checks; the release gate runs the full integration suite and production build.

Configuration, logging, and operations

  • Use @t3-oss/env-nextjs with Zod 4, split server/client schemas, and build/startup validation.
  • Put Pino behind packages/observability and emit structured JSON through OpenTelemetry into Vercel, with Sentry for errors, releases, and source maps.
  • Telemetry is allowlist-only: release, environment, route template, operation, duration, status, request/trace IDs, and opaque job IDs. Exclude email, Collector and record identifiers, query strings, bodies, database values, filenames, Passkey material, and workflow payloads.
  • Disable Sentry PII/IP collection, attachments, feedback, profiling, and replay. Do not enable Vercel Web Analytics, custom behavior events, or browser session tracing in v1.
  • Retain operational telemetry for 30 days and the separate security audit for 90 days. Alert on error rate, latency, database health, workflow failures, outbox age, PITR health, and Blob failures.

Accepted v1 limitations

  • Rely on Neon 30-day point-in-time recovery. Independent cross-provider database backups and restore drills are deferred.
  • Rely on provider durability for Private Blob plus Collector-held Complete Native Exports. Accidental Blob deletion or provider loss can therefore be unrecoverable in v1.
  • Email verification enforcement and email-based recovery are out of scope.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment