Skip to content

Migrate backend to Deno 2, harden auth, and finalize the template - #6

Merged
Adjanour merged 27 commits into
masterfrom
feat/auth-security-and-docker
Aug 3, 2026
Merged

Adjanour merged 27 commits into
masterfrom
feat/auth-security-and-docker

Conversation

@Adjanour

@Adjanour Adjanour commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates the backend to a Deno 2 workspace with hardened auth and Docker-based deployment, then brings the template to a genuinely finished, mergeable state — working dev environment, real (not stubbed) batteries, CI coverage for the frontend, test coverage where there was none, and clean lint/format/types repo-wide.

Motivation

The stack needed to move off Node/pnpm-only tooling onto Deno while keeping the frontend on Vite/pnpm, and harden auth (2FA, argon2, rate limiting). Once that migration landed, a follow-up pass closed the gap between what the README/docs advertised as "batteries included" and what actually worked end-to-end, so the template is safe to hand to a new team without hidden landmines.

Changes Made

Migration (pre-existing commits on this branch):

  • Backend moved to Deno 2 (workspace, Hono, Drizzle, better-auth), argon2 password hashing, 2FA, rate limiting
  • Frontend Docker image (Deno builder + Caddy) added alongside the existing Vite/pnpm toolchain
  • CI/CD workflows updated for the workspace + frontend Docker deploy

Finalization (this session, 9 commits):

  • fix(infra): dev docker-compose.yml was missing postgres/redis services entirely — the README's own quickstart (docker compose up -d) couldn't actually start a database. Added, mirroring docker-compose.prod.yml's shape.
  • feat(jobs): the background-jobs battery was fake — email/cleanup/sync worker processors were all // TODO stubs and nothing called addJob. Implemented a real email processor, dropped cleanup/sync (no concrete meaning in a template), and routed better-auth's sign-up/reset-password emails through a new queueEmail helper that queues via Redis when available and sends inline otherwise — keeping email and background-jobs independently optional, matching how the rest of the codebase treats Redis (e.g. withCache).
  • chore: normalized deno.json formatting (tabs vs 2-space were inconsistent across the workspace) and resolved a live conflict between Biome's and Deno's formatters over the same files.
  • ci: added frontend lint/typecheck/build to CI (previously untested — a broken frontend could merge to master unnoticed), added deno fmt --check, pinned the Deno version to match what the Dockerfiles already pin instead of floating.
  • test(db): packages/db had zero tests; added schema/type-level coverage and fixed a stale UserRole type ("user" | "admin" vs the actual "buyer" | "seller" | "admin" enum).
  • test(frontend): apps/frontend had zero tests; added a Deno-native test setup (jsdom + Testing Library, no Vitest) with one pure-function test and one component test, matching the convention already established for packages/shared/packages/email-templates.
  • docs: filled remaining gaps — env var reference, and two config "inconsistencies" that turned out to be intentional (documented instead of changed).
  • fix: resolved 12 pre-existing deno lint require-await errors, unrelated to any of the above, blocking a clean deno lint run.

Testing Instructions

docker compose up -d db redis   # or: docker compose up -d for the full stack
deno task db:migrate
deno lint                       # clean
deno fmt --check                # clean
deno check apps/backend/src/index.ts apps/backend/src/jobs/worker.ts
deno test -A                    # backend + packages/shared + packages/db + packages/email-templates
deno task test:frontend         # frontend
cd apps/frontend && deno task typecheck && deno task build

All of the above pass locally as of this branch. CI runs the same set (plus Biome for the frontend).

Pre-Submission Checklist

  • All tests pass — deno test -A
  • No type errors — deno check
  • No lint warnings — deno lint
  • Tests added or updated for new functionality
  • Branch is up to date and pushed
  • Commit history tells a legible story

Technical Decisions

  • queueEmail degrade pattern (apps/backend/src/lib/email.ts): routing auth emails through addJob unconditionally would have made email hard-require Redis, silently breaking the documented "email works with just RESEND_API_KEY" contract. queueEmail checks for a configured Redis client and falls back to sending inline.
  • Deno-native frontend tests over Vitest: this branch already dropped standalone vitest.config.ts from packages/shared/packages/email-templates in favor of deno test + @std/testing/bdd + @std/expect. The frontend test setup follows the same convention (npm:jsdom + npm:@testing-library/react under deno test) rather than reintroducing a second test runner.
  • apps/frontend/tsconfig.json and apps/backend/deno.json's module: NodeNext were flagged as inconsistencies in an initial audit but turned out to be load-bearing (tsc/Vite's tsconfigPaths, and correct typing for CJS-shaped npm deps like bullmq/ioredis/pino) — documented in docs/DENO_WORKSPACE_SCOPE.md rather than changed.

Related Work

N/A

Reviewer Notes

  • This PR bundles the original Node→Deno migration (9 commits, ~110 files) with a finalization pass (9 more commits, smaller/focused diffs each). Given the 117-file total, reviewing commit-by-commit will be far more tractable than the full diff — each finalization commit is a self-contained, coherent unit.
  • Found during verification, not fixed here (informational only, doesn't affect CI or other machines): deno task db:migrate can appear to fail on a dev machine that already runs a native (non-Docker) Postgres on port 5432 with a same-named orcta_dev database from an unrelated project — it's a local port collision, not a bug in the migration or drizzle-orm. Confirmed clean and idempotent against an isolated Postgres.

Adjanour added 18 commits May 10, 2026 02:05
…r optimization

Replace Node.js toolchain with native Deno (remove tsconfig, vitest, SWC,
pnpm). Install Better Auth with Argon2id password hashing, email verification,
password reset flow, and TOTP 2FA with backup codes/trusted devices. Add Redis
secondary storage for sessions, cookie cache, and rate limiting.

The Dockerfile is rewritten with multi-stage deno compile producing a standalone
binary (~50MB vs ~170MB).
…CORS and error boundary

- Restore root workspace with all 5 members (frontend, backend, 3 packages)
- Remove nodeModulesDir from backend (only valid at root per Deno docs)
- Remove explicit @repo/* import maps (resolve via workspace now)
- Add frontend Dockerfile (Deno builder + Caddy runner with SPA + /api/* proxy)
- Add docker-compose.yml with frontend (port 80) and backend (port 9999)
- Fix CORS: authClient and api default to same-origin via Vite proxy
- Add ErrorFallback and 404 components to root route
- Update backend Dockerfile to Deno 2.7.14 Debian with workspace member stubs
- Remove .dockerignore frontend exclusion
- Lower min password length to 8
- Strip .ts extensions from backend auth imports
…hitecture

- Rewrite DENO_WORKSPACE_SCOPE.md from migration plan to current-state doc
- Update README.md: frontend on :3000, Docker Compose layout, Deno-only prereq
- Add Docker Compose full-stack option to DEPLOYMENT.md
- Fix broken CLAUDE.md link in AGENTS.md to point to existing docs
…d cleanup root package.json

- Flatten usecases file in new-module.sh (matches existing users module pattern)
- Add apps/frontend to pnpm-workspace.yaml so pnpm installs frontend deps
- Remove stale pnpm scripts and engines from root package.json
- Update setup.sh copy to reference Deno workspace resolution
- Fix FRONTEND_URL port in CI (5173 → 3000)
- Add frontend image build and push to deploy workflow
- Add frontend service to docker-compose.prod.yml (Caddy on :80)
- Remove accidentally tracked generated routeTree.gen.ts
- Add routeTree.gen.ts to .gitignore
Aligns with Orcta PR guidelines (Summary, Motivation, Changes Made,
Testing, Screenshots, Pre-Submission Checklist, Technical Decisions,
Related Work, Reviewer Notes) and the AGENTS.md PR format.
The README quickstart and scripts/setup.sh both assume `docker compose
up -d` starts Postgres and Redis, but the compose file only defined
frontend/backend build services — a fresh clone couldn't actually get
a database running. Mirrors docker-compose.prod.yml's db/redis shape
(image, healthcheck) with dev-appropriate host-exposed ports so
`deno task db:migrate` and `deno task dev` running on the host can
reach them at the .env.example defaults.
cleanup/sync had no concrete meaning in a template (no domain to clean
up or sync) and were pure TODO stubs — dropped rather than filled with
speculative logic. email is now real: worker.ts looks up the template
by name and sends it via the existing Resend wrapper.

Routes better-auth's sendResetPassword/sendVerificationEmail through
a new queueEmail helper (apps/backend/src/lib/email.ts) instead of
calling sendEmail synchronously, so auth requests no longer block on
the Resend API call. queueEmail queues through BullMQ when REDIS_URL
is set and sends inline otherwise, keeping email and background jobs
independently optional the way withCache already treats Redis for
caching — addJob alone would have made email hard-require Redis,
which isn't how either battery is documented.

Updates docs/BATTERIES.md's Background Jobs and Email sections to
match.
Root, apps/backend, and apps/frontend deno.json used tabs and failed
deno fmt --check; the three packages/*/deno.json already used 2-space
and passed. Invisible until now because CI never ran deno fmt --check.
Ran deno fmt on the three offenders so all six deno.json files agree.

Biome's formatter (indentStyle: tab, applied repo-wide with no path
scoping) was fighting Deno's fmt on these same files, so excludes
**/deno.json and **/deno.lock via a biome.json override — Deno owns
its own config format, Biome owns everything else.
CI never touched the frontend (no lint/typecheck/build) and never ran
deno fmt --check, so both could silently break on master. Adds
deno install + biome ci (pinned to the same 2.3.7 as the root
devDependency) + tsc --noEmit + vite build for apps/frontend, mirroring
apps/frontend/Dockerfile's actual build path. Pins setup-deno's
deno-version to 2.7.14 to match what both Dockerfiles already pin,
instead of floating v2.x.

Enabling deno fmt --check surfaced that it disagreed with Biome across
apps/frontend (tabs, Biome's territory) and reflowed markdown prose in
docs — excluded both via deno.json's fmt.exclude, plus
packages/db/migrations (drizzle-kit generated, shouldn't be hand
-formatted, consistent with biome.json's existing exclusion for the
same directory). The two remaining genuine strays (root package.json,
packages/db/src/schema/users.ts, plus a line-wrap in the jobs/index.ts
from the previous commit) are reformatted to match.
UserRole was hardcoded to "user" | "admin" while the actual
userRoleEnum is ["buyer", "seller", "admin"] — now derived from the
enum's own values instead of hand-maintained, so it can't drift again.

packages/db had zero tests. Adds schema/type-level coverage for
userRoleEnum and the drizzle-zod insert/select schemas, following
packages/shared's existing describe/it + @std/expect convention — no
DB connection, matching how packages/db has no live client of its own
(that lives in apps/backend/src/db).
… unit test

apps/frontend had zero tests. This migration branch already dropped
standalone vitest.config.ts from packages/shared and packages/email
-templates in favor of deno test + @std/testing/bdd + @std/expect, so
the frontend follows the same convention rather than reintroducing
Vitest — npm:jsdom for a DOM environment, npm:@testing-library/react
for rendering, installed via apps/frontend/src/test-setup.ts (Deno's
runner has no Vitest-style global setupFiles, so it's imported
explicitly per test file).

Two examples, mirroring the backend's prepareEmailChange as the
canonical pure-function pattern:
- lib/utils.ts's cn() — pure, no DOM
- components/ui/field-error.tsx — simplest real component with actual
  conditional logic and no router/query context dependency

apps/frontend/tsconfig.json now excludes test files — tsc has no way
to resolve @std/* JSR specifiers (they're Deno-only), and test files
never go through Vite's build anyway, so deno test/check remains the
one authoritative type-checker for them. Also adds a root
test:frontend task mirroring the existing dev/dev:frontend split.
- Note why apps/frontend/tsconfig.json coexists with deno.json (tsc
  --noEmit and Vite's tsconfigPaths both need it; deno.json's own
  compilerOptions serve deno check/test/LSP separately) and why
  apps/backend/deno.json sets module: NodeNext (matches the CJS shape
  of bullmq/ioredis/pino/@aws-sdk/*) — both flagged as inconsistencies
  in an earlier audit pass, neither is one.
- packages/email-templates/deno.json was missing the test.include
  block every other package declares, despite having a real
  __tests__ dir already.
- docs/DEPLOYMENT.md's env var reference was missing SERVER_URL and
  the Google/GitHub OAuth vars that .env.example documents, and had
  no pointer to docs/BATTERIES.md for what each var actually does.
deno lint's require-await rule was failing repo-wide, unrelated to
this branch's other work — likely surfaced by a Deno version
difference from whenever these were last verified clean.

- pingHandler, getUploadUrl/getDownloadUrl, and the rate-limit
  middleware factory return synchronously (or return an already
  -Promise-returning call directly) and never needed async in the
  first place — dropped it.
- andThenAsync (packages/shared) genuinely needs to normalize a mixed
  sync/async return into one Promise — that's a correct use of async
  without an explicit await, but the lint rule can't tell the
  difference from a mistake. Added an explicit await on the async
  branch, which is behavior-preserving and satisfies the rule for
  real rather than suppressing it.
- The test callbacks passed to andThenAsync (in both apps/backend's
  and packages/shared's result.test.ts) only needed to return a
  Promise, not be declared async — switched them to
  `() => Promise.resolve(...)`.
Copilot AI review requested due to automatic review settings August 3, 2026 10:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes a broad migration of the repo to a Deno 2 workspace (backend + shared packages) while keeping the frontend on Vite, and it hardens/authenticates the stack (Better Auth + 2FA + Redis-backed rate limiting) alongside Docker-based deployment and CI updates.

Changes:

  • Move backend runtime/tooling to Deno 2 (workspace deno.json, Deno tasks, Deno.serve, compiled backend Docker image).
  • Replace Vitest/tsc-built package workflow with Deno-native packages (packages/* now export via deno.json) and Deno test runner across backend + packages.
  • Add/adjust infra and docs for full-stack Docker Compose deploy, frontend Docker build + proxy, and CI checks (fmt/lint/check/tests + frontend build/typecheck).

Reviewed changes

Copilot reviewed 101 out of 117 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tsconfig.json Removed root TypeScript project config (superseded by Deno workspace).
tsconfig.base.json Removed shared TS base config (superseded by Deno workspace).
scripts/setup.sh Setup script updated for Deno-based workflow and new tasks.
scripts/new-module.sh Module scaffolder adjusted for Deno stdlib tests + new layout.
README.md Updated quickstart, commands, ports, and “batteries” usage for Deno/Docker changes.
pnpm-workspace.yaml pnpm workspace narrowed to frontend + packages scope.
packages/shared/vitest.config.ts Removed Vitest config (migrated to deno test).
packages/shared/tsconfig.json Removed package TS config (migrated to deno.json).
packages/shared/src/types.ts Formatting + Deno-first TS adjustments.
packages/shared/src/schemas.ts Formatting + pagination/helpers preserved under Deno.
packages/shared/src/result.ts Fix andThenAsync to actually await (lint correctness).
packages/shared/src/index.ts Switch exports from .js to .ts entrypoints.
packages/shared/src/tests/schemas.test.ts Migrated tests from Vitest to @std/testing + @std/expect.
packages/shared/package.json Reduced to minimal package metadata post-Deno migration.
packages/shared/deno.json New Deno package definition + exports/imports.
packages/email-templates/vitest.config.ts Removed Vitest config (migrated to deno test).
packages/email-templates/tsconfig.json Removed TS config (migrated to deno.json).
packages/email-templates/src/index.ts Formatting cleanup; keep template functions Deno-friendly.
packages/email-templates/src/tests/index.test.ts Migrated tests from Vitest to Deno std test/expect.
packages/email-templates/package.json Reduced to minimal package metadata post-Deno migration.
packages/email-templates/deno.json New Deno package definition + exports + test include.
packages/db/tsconfig.json Removed TS build config (migrated to deno.json).
packages/db/src/types.ts Align exports/types with schema (incl. UserRole).
packages/db/src/schema/users.ts Update role enum + add 2FA-related columns/types.
packages/db/src/schema/sessions.ts Formatting cleanup + .ts import normalization.
packages/db/src/tests/schema.test.ts Added type/schema-level tests for DB schema.
packages/db/package.json Reduced to minimal package metadata post-Deno migration.
packages/db/migrations/meta/0000_snapshot.json Formatting normalization for migration metadata.
packages/db/migrations/meta/_journal.json Add new migration journal entry.
packages/db/migrations/0001_curious_fantastic_four.sql New migration for role enum + 2FA columns.
packages/db/drizzle.config.ts Adjust drizzle config for updated environment handling.
packages/db/deno.json New Deno package definition + exports/imports + test include.
package.json Root scripts/dev deps simplified; keep Biome for frontend linting.
docs/WRITING.md Formatting/structure refinements.
docs/DEPLOYMENT.md Expanded deployment guide including full-stack Compose + updated commands.
docs/DENO_WORKSPACE_SCOPE.md New doc describing workspace architecture and tradeoffs.
docker-compose.yml Dev compose expanded to include frontend/backend + DB/Redis health deps.
docker-compose.prod.yml Prod compose updated to include frontend image + internal backend exposure.
deno.json New root Deno workspace + tasks + shared imports + fmt excludes.
CONTRIBUTING.md Updated contributor workflow for Deno-based toolchain.
CLAUDE.md Removed legacy assistant quick-reference doc.
biome.json Formatting + override updates (incl. skipping deno.json/deno.lock).
apps/frontend/vite.config.ts Update Vite config to explicit root/routes dir and built-in tsconfig paths.
apps/frontend/tsconfig.json Exclude test files/setup from tsc --noEmit.
apps/frontend/src/test-setup.ts Add jsdom-based setup for deno test + Testing Library.
apps/frontend/src/routes/__root.tsx Add root error/not-found components and route config tweaks.
apps/frontend/src/lib/auth-client.ts Remove default baseURL fallback; rely on configured VITE_API_URL.
apps/frontend/src/lib/api.ts Default API base URL changed to empty string fallback.
apps/frontend/src/lib/tests/utils.test.ts Added a pure-function test under Deno runner.
apps/frontend/src/components/ui/field-error.tsx Normalize import to .ts extension.
apps/frontend/src/components/ui/tests/field-error.test.tsx Added component test using Testing Library under Deno.
apps/frontend/src/components/error-fallback.tsx New shared error fallback UI component.
apps/frontend/package.json Adjust dev port/build script; add jsdom/testing-library deps.
apps/frontend/Dockerfile New frontend Docker build (Deno builder + Caddy runtime).
apps/frontend/deno.json New frontend Deno config (tasks for Vite, test, typecheck).
apps/frontend/Caddyfile SPA serving + /api/* reverse proxy to backend service.
apps/backend/vitest.config.ts Removed Vitest config (migrated to deno test).
apps/backend/tsconfig.json Removed backend TS config (migrated to apps/backend/deno.json).
apps/backend/src/routes/index.ts Formatting cleanup in route registration.
apps/backend/src/modules/users/users.usecases.ts Import normalization + small logic formatting adjustments.
apps/backend/src/modules/users/users.repository.ts Import normalization + formatting; keep tryInfra pattern.
apps/backend/src/modules/users/routes.ts Formatting/indent normalization for OpenAPI routes.
apps/backend/src/modules/users/index.ts Normalize explicit .ts imports and chaining formatting.
apps/backend/src/modules/users/handlers.ts Refactor/format handlers; add email-change flow integration.
apps/backend/src/modules/users/tests/users.usecases.test.ts Migrate tests to Deno std test/expect.
apps/backend/src/modules/users/tests/users.repository.test.ts Migrate tests to Deno std test/expect + cleanup adjustments.
apps/backend/src/modules/health/usecases/check-health.usecase.ts Formatting normalization.
apps/backend/src/modules/health/routes.ts Formatting normalization for OpenAPI routes.
apps/backend/src/modules/health/index.ts Normalize explicit .ts imports and chaining formatting.
apps/backend/src/modules/health/handlers.ts Normalize imports; remove unnecessary async from ping handler.
apps/backend/src/middlewares/wide-event.ts Formatting + minor control-flow braces normalization.
apps/backend/src/middlewares/auth.ts Harden auth context typing + default role to new enum baseline.
apps/backend/src/lib/ws.ts Formatting normalization.
apps/backend/src/lib/types.ts Normalize .ts imports and formatting for shared handler utilities.
apps/backend/src/lib/storage.ts Minor refactor to remove unnecessary async on URL helpers.
apps/backend/src/lib/redis.ts Update ioredis import style + formatting.
apps/backend/src/lib/rate-limit.ts Remove unnecessary async wrapper; formatting normalization.
apps/backend/src/lib/infra.ts Normalize .ts imports and formatting.
apps/backend/src/lib/http-status-phrases.ts Formatting normalization.
apps/backend/src/lib/error.ts Use override cause typing; formatting normalization.
apps/backend/src/lib/email.ts New email sending + queueEmail degrade behavior (Redis optional).
apps/backend/src/lib/create-app.ts Make app creation async; Deno stdout logging + optional Axiom stream.
apps/backend/src/lib/configure-open-api.ts Update types to match async app creation.
apps/backend/src/lib/cache.ts Formatting normalization.
apps/backend/src/lib/auth.ts Add argon2 hashing, 2FA plugin, rate limiting config, email hooks, secondary storage.
apps/backend/src/jobs/worker.ts Implement real email worker; remove stub processors; Deno signal shutdown.
apps/backend/src/jobs/index.ts Simplify to email-only job type + shared template registry + addJob helper.
apps/backend/src/index.ts Switch server startup from Node Hono server to Deno.serve with graceful shutdown.
apps/backend/src/env.ts Move env reading to Deno.env; adjust schema parsing/exit behavior.
apps/backend/src/db/migrate.ts Make migrations runnable under Deno with URL-based migrations path.
apps/backend/src/app.ts Await async app creation; normalize route imports and middleware wiring.
apps/backend/package.json Reduce backend package.json now that Deno owns runtime.
apps/backend/drizzle.config.ts Update drizzle-kit config to read DATABASE_URL via Deno.env.
apps/backend/Dockerfile Build backend as compiled Deno binary; slim Alpine runtime.
apps/backend/deno.json New backend Deno config with tasks/imports and NodeNext typing settings.
apps/backend/.swcrc Removed SWC config (no longer building with SWC).
.vscode/settings.json Enable Deno LSP + adjust workspace editor settings.
.vscode/extensions.json Recommend Deno extension alongside Biome/TanStack/Tailwind tooling.
.gitignore Ignore additional generated files and updated workspace artifacts.
.github/workflows/deploy.yml Build/push backend + frontend images; update VPS deploy steps.
.github/workflows/ci.yml Switch CI to Deno fmt/lint/check/test + frontend build/typecheck/lint.
.github/PULL_REQUEST_TEMPLATE.md New PR template aligned with Deno toolchain commands.
.dockerignore Ignore tests/docs/scripts in Docker contexts; adjust build exclusions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/deploy.yml Outdated
Comment on lines +136 to +137
# Run database migrations
IMAGE_TAG=latest docker compose --env-file .env.production -f docker-compose.prod.yml run --rm backend deno run --allow-env --allow-net --allow-read --allow-sys apps/backend/src/db/migrate.ts
Comment thread README.md Outdated
| `handlers.ts` | HTTP handlers — reads input, calls repo, maps Result to response |
| `posts.repository.ts` | Data access — uses `tryInfra`, returns `Result`, never throws |
| `posts.errors.ts` | Typed domain error variants (`PostNotFound`, etc.) |
| `usecases/` | Pure business logic — no DB, no async, fully unit-testable |
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment on lines +1 to +6
ALTER TABLE "users" ALTER COLUMN "role" SET DATA TYPE text;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "role" SET DEFAULT 'buyer'::text;--> statement-breakpoint
DROP TYPE "public"."user_role";--> statement-breakpoint
CREATE TYPE "public"."user_role" AS ENUM('buyer', 'seller', 'admin');--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "role" SET DEFAULT 'buyer'::"public"."user_role";--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "role" SET DATA TYPE "public"."user_role" USING "role"::"public"."user_role";--> statement-breakpoint
Comment thread scripts/setup.sh Outdated
NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
if [[ "$NODE_MAJOR" -lt 20 ]]; then
error "Node.js v${NODE_MAJOR} found — v20 or higher required."
DENO_MAJOR=$(deno --version | head -1 | grep -oP '\d+' | head -1)
Comment thread apps/frontend/Dockerfile Outdated
Comment on lines +6 to +8
# Layer 1: Config files (changes infrequently — preserves dep cache)
COPY apps/frontend/deno.json apps/frontend/package.json ./
RUN deno install
Comment thread scripts/new-module.sh
Comment on lines +119 to +120
# ── ${MODULE}.usecases.ts ──────────────────────────────────────────────────────
cat > "${MODULE_DIR}/${MODULE}.usecases.ts" << EOF
better-auth applies a built-in 3-requests-per-10s rule to /sign-in,
/sign-up, /change-password, and /change-email regardless of the
configured max. With storage: "secondary-storage" and no Redis
configured, that storage layer silently no-ops (optional chaining on
an undefined secondaryStorage), so the limit was never actually
enforced anywhere except environments with Redis — including CI,
which provisions Redis and legitimately signs up/in many test users
in quick succession across handlers.test.ts, tripping the limit and
returning 429s where 200/403/404 were expected.

Disabling rate limiting under NODE_ENV=test keeps the production
protection intact while letting the integration suite run at the
speed it always has locally (where this was silently never enforced).
…compat

Two independent CI failures, both real:

1. jsdom@30 (via its undici@8.9.0 dependency) calls
   webidl.util.markAsUncloneable, a Node-compat API Deno 2.7.14 doesn't
   provide — reproduced locally by installing that exact version.
   Deno 2.8.0 has it; bumped CI's pin to 2.9.0. This only affects
   test-tooling — production images never run `deno test`, so the
   Dockerfiles' 2.7.14 pin is unaffected and unchanged.

2. Running `deno test -A` from the root swept up apps/frontend's
   jsdom-based tests into the same process as the backend's. The
   webidl crash above was an uncaught rejection that corrupted the
   shared fetch/undici runtime mid-run, which is why unrelated backend
   handlers.test.ts assertions failed in the same CI run — they share
   process state with a frontend test suite that was never designed to
   run alongside them. Split into two steps: "Test" now explicitly
   excludes apps/frontend, and a new "Frontend test" step runs it via
   the existing deno task test, isolated in its own process.
Both were non-obvious enough to burn real debugging time finding —
worth writing down so the next person (or future me) doesn't
re-derive them from a confusing CI failure:

- apps/backend/docs/DECISIONS.md: why better-auth's rate limiter was
  silently inert everywhere except Redis-backed environments, and why
  disabling it under NODE_ENV=test is the fix rather than a workaround.
- docs/DENO_WORKSPACE_SCOPE.md: the jsdom/undici/Deno-version
  incompatibility (with the exact repro), and why apps/frontend's
  tests need to run in their own `deno test` process rather than
  getting swept into a root-level `deno test -A`.
…nd image

apps/backend/Dockerfile compiles to a standalone binary on bare
alpine:3.20 — no deno executable, no source tree. deploy.yml's
migration step ran `docker compose run --rm backend deno run ...
migrate.ts` against that image, which would fail on the first deploy
that ships a schema change. Verified by inspecting the built image and
reproducing the failure mode locally.

Adds a `migrate` service to docker-compose.prod.yml using the same
Deno base image apps/backend/Dockerfile builds from, mounting the
already-checked-out repo on the VPS instead of baking source into an
image. profiles: ["tools"] keeps it from ever starting via `docker
compose up`. Verified end-to-end locally: `docker compose -f
docker-compose.prod.yml run --rm migrate` against a real Postgres
applies both migrations and reports "Migrations complete."

Also adds .env.production to .gitignore — docker-compose.prod.yml's
own header comment already claimed it's "never committed to git," but
nothing actually enforced that.
Migration 0001 drops the old user_role enum ('user'|'admin') and casts
the column into the new one ('buyer'|'seller'|'admin') via USING
"role"::"public"."user_role". Any existing row with role = 'user' has
no matching value in the new enum — the cast aborts and the migration
fails partway through.

Verified by seeding a row with role = 'user' against a fresh database,
running the unpatched migration (fails: invalid input value for enum
user_role), then the patched one (succeeds, row becomes 'buyer' —
matching the column's own new default).

Only matters for a database that already had migration 0000 applied
before this one exists; harmless no-op on a fresh install.
The frontend Dockerfile copied only apps/frontend's own deno.json and
package.json before running deno install — no root deno.json, no
lockfile, no sibling members. Deno correctly treats that as a
standalone project, not the workspace it actually belongs to, and
silently re-resolves everything: verified locally that better-auth and
@better-fetch/fetch drifted to newer versions than the checked-in
deno.lock pins, on every single build.

Copying just deno.lock alongside isn't enough either — same drift,
confirmed the same way. The lockfile only stays exact once the full
workspace skeleton is present (root deno.json + deno.lock +
package.json, plus every member's own deno.json, matching what
apps/backend/Dockerfile already does correctly). Verified by
replicating the exact copied file layout in a plain directory (no
Docker needed for this part) and diffing deno.lock before/after
install — identical only with the full skeleton in place.

Running deno install/build scoped to apps/frontend (rather than the
whole workspace) keeps the frontend image from pulling in backend-only
npm deps it doesn't need. Also updates the final stage's COPY path
(dist/ now lives at apps/frontend/dist/, not the repo root, since the
build no longer flattens frontend's files into /app directly) — the
full install→build pipeline was verified outside Docker to confirm
this new path is correct.
- README.md, CONTRIBUTING.md: frontend dev server moved to :3000 a
  while back; both still said :5173.
- README.md, scripts/new-module.sh: the scaffolder generates
  `<module>.usecases.ts` at the module root, not a `usecases/`
  directory — the module table and the scaffolder's own success
  message both still described the old layout.
grep -oP relies on GNU grep's PCRE flag, unavailable on macOS/BSD
grep by default — setup.sh would fail parsing the Deno major version
on a stock macOS machine. grep -oE '[0-9]+' is POSIX-portable and
extracts the same first match.
Test and Frontend test were sequential steps in one job — never
actually concurrent, just visually separated. Splits into two jobs
with no needs: dependency between them, so GitHub Actions schedules
them on separate runners simultaneously instead of one after another.

backend keeps the postgres/redis services (needed for migrations and
the test suite); frontend doesn't need either, so it no longer waits
on service containers it never touches. Lint now excludes
apps/frontend explicitly, since Biome (checked in the frontend job) is
the linter of record there — deno lint was already implicitly
scanning those files redundantly.

No branch protection rule references the old job name ("check"), so
renaming to backend/frontend doesn't affect required-check gating.
@Adjanour
Adjanour merged commit 00245cc into master Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants