Migrate backend to Deno 2, harden auth, and finalize the template - #6
Merged
Merged
Conversation
…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(...)`.
Contributor
There was a problem hiding this comment.
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 viadeno.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 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 |
| | `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 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 |
| 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 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 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
Finalization (this session, 9 commits):
fix(infra): devdocker-compose.ymlwas missingpostgres/redisservices entirely — the README's own quickstart (docker compose up -d) couldn't actually start a database. Added, mirroringdocker-compose.prod.yml's shape.feat(jobs): the background-jobs battery was fake —email/cleanup/syncworker processors were all// TODOstubs and nothing calledaddJob. Implemented a realemailprocessor, droppedcleanup/sync(no concrete meaning in a template), and routed better-auth's sign-up/reset-password emails through a newqueueEmailhelper 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: normalizeddeno.jsonformatting (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), addeddeno fmt --check, pinned the Deno version to match what the Dockerfiles already pin instead of floating.test(db):packages/dbhad zero tests; added schema/type-level coverage and fixed a staleUserRoletype ("user" | "admin"vs the actual"buyer" | "seller" | "admin"enum).test(frontend):apps/frontendhad 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 forpackages/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-existingdeno lintrequire-awaiterrors, unrelated to any of the above, blocking a cleandeno lintrun.Testing Instructions
All of the above pass locally as of this branch. CI runs the same set (plus Biome for the frontend).
Pre-Submission Checklist
deno test -Adeno checkdeno lintTechnical Decisions
queueEmaildegrade pattern (apps/backend/src/lib/email.ts): routing auth emails throughaddJobunconditionally would have made email hard-require Redis, silently breaking the documented "email works with justRESEND_API_KEY" contract.queueEmailchecks for a configured Redis client and falls back to sending inline.vitest.config.tsfrompackages/shared/packages/email-templatesin favor ofdeno test+@std/testing/bdd+@std/expect. The frontend test setup follows the same convention (npm:jsdom+npm:@testing-library/reactunderdeno test) rather than reintroducing a second test runner.apps/frontend/tsconfig.jsonandapps/backend/deno.json'smodule: NodeNextwere flagged as inconsistencies in an initial audit but turned out to be load-bearing (tsc/Vite'stsconfigPaths, and correct typing for CJS-shaped npm deps likebullmq/ioredis/pino) — documented indocs/DENO_WORKSPACE_SCOPE.mdrather than changed.Related Work
N/A
Reviewer Notes
deno task db:migratecan appear to fail on a dev machine that already runs a native (non-Docker) Postgres on port 5432 with a same-namedorcta_devdatabase from an unrelated project — it's a local port collision, not a bug in the migration ordrizzle-orm. Confirmed clean and idempotent against an isolated Postgres.