This document defines the default local workflow for the current monorepo.
Important note:
- The current product scope is defined by
docs/mvp-scope.md - The old
topic/city/applicationsimplementation has been retired from the active code path; remaining mentions are migration history and route-retirement tests
Local development should let us:
- start the shared infrastructure we need now
- bootstrap database schema and baseline content
- create the first authenticated staff account
- run the public site, admin console, and API independently
Node.js >= 22Docker Desktopor another working Docker runtime- repository dependencies installed with:
env COREPACK_HOME="$PWD/.corepack" corepack pnpm install
Create the root env file before running auth-backed API flows:
cp .env.example .envImportant local variables:
DATABASE_URL- local PostgreSQL connection string
BETTER_AUTH_SECRET- required for
/api/auth/*and staff session handling
- required for
BETTER_AUTH_URL- auth base URL, defaults to the local API origin
LOG_FORMAT- optional API log output format:
logfmtorjson
- optional API log output format:
INTERNAL_API_TOKEN- required for
/api/internal/v1/*trusted automation routes such as scheduled publishing
- required for
DEV_ADMIN_EMAILDEV_ADMIN_PASSWORDDEV_ADMIN_NAME- used by the admin bootstrap script
S3_ENDPOINTS3_BUCKETS3_ACCESS_KEY_IDS3_SECRET_ACCESS_KEY- required for admin asset uploads
S3_REGIONS3_FORCE_PATH_STYLES3_PUBLIC_BASE_URLASSET_UPLOAD_EXPIRES_IN_SECONDS- optional storage tuning for local MinIO or other S3-compatible providers
PUBLIC_WRITE_RATE_LIMIT_WINDOW_SECONDSPUBLIC_APPLICATION_RATE_LIMIT_MAXPUBLIC_EVENT_REGISTRATION_RATE_LIMIT_MAX- baseline in-memory throttles for public write endpoints
ASSET_IMAGE_MAX_DIMENSIONASSET_IMAGE_MAX_PIXELS- server-side image metadata limits enforced during asset finalization
Start PostgreSQL:
npm run infra:upStart optional local object storage:
npm run infra:up:storageUseful supporting commands:
npm run infra:logs
npm run infra:downCurrent expectation:
infra:up- starts
postgres
- starts
infra:up:storage- starts
minioand the one-shot bucket setup job - creates the local bucket and enables public reads for public asset previews
- starts
Use the one-command bootstrap for a fresh local database:
npm run bootstrap:devThis runs:
npm run db:migratenpm run db:seednpm run auth:bootstrap-admin
If you want the local database to use the scraped official TGO branch and event dataset instead of the demo event set, use:
npm run bootstrap:tgo-infoqThis runs:
npm run db:migratenpm run db:seednpm run db:cleanup:demo-eventsnpm run db:import:tgo-infoqnpm run db:sync-homepage:tgo-infoqnpm run auth:bootstrap-admin
If you need the steps separately:
npm run db:migrate
npm run db:seed
npm run auth:bootstrap-adminBootstrap result:
- schema is migrated
- baseline demo content, roles, and permissions are inserted
- a development super admin account is created or refreshed from
.env
Official bootstrap result:
- roles, permissions, site pages, homepage shell, and admin account bootstrap stay unchanged
- seeded demo/test events are removed from the local database
data/imports/tgo-infoq/is imported intobranches,branch_board_members,events,event_sessions, andassets- homepage featured branches, featured events, and metrics are synchronized to the imported dataset
Run each application independently:
npm run dev:api
npm run dev:site
npm run dev:adminDefault local URLs:
- Public site:
http://localhost:4321 - Admin console:
http://localhost:5173 - API service:
http://localhost:8787
You can also run the three apps together:
npm run devAutomated backend coverage is now available before or after the manual checklist:
npm run test:apiAstro-side public data-loading coverage is also available:
npm run test:siteBrowser-level smoke coverage is also available:
npm run test:e2eWhat the integration suite does:
- creates a temporary PostgreSQL database derived from
DATABASE_URL - runs Drizzle migrations and seed data into that database
- exercises auth, permission checks, staff and role management, and scheduled publishing
- exercises public list/detail, application, registration, and rate-limit behavior
- drops the temporary database after the run finishes
What the Astro helper suite does:
- mocks public API fetch requests in Node
- verifies configured API base URL handling
- verifies fallback to shared demo content when public requests fail
What the Playwright smoke suite does:
- starts or reuses local
postgresthroughnpm run infra:up - reapplies local migrations, seed data, and the bootstrapped super admin account
- starts
api,site, andadmindev servers on fixed localhost ports - verifies admin login, protected-route redirect behavior, public homepage rendering, public application submission, and public event registration submission
Browser note:
- on macOS, local runs prefer an installed
Google Chrome.appwhen available - if you prefer Playwright-managed browsers, run
npm run test:e2e:installonce
Important note:
- never point
DATABASE_URLat a production instance when running the integration suite
Recommended minimum verification after bootstrap:
- confirm successful responses return an
X-Request-IDheader - confirm error responses also include
error.requestIdfor log correlation - run
npm run test:e2ewhen auth, public forms, or cross-app navigation changes
The route list below reflects the current local implementation baseline. It is useful for development smoke tests, but it is not the canonical product-scope document.
GET /GET /healthGET /readyGET /versionGET /api/public/v1/site-configGET /api/public/v1/homeGET /api/public/v1/branchesGET /api/public/v1/membersGET /api/public/v1/articlesGET /api/public/v1/eventsGET /api/public/v1/joinGET /api/public/v1/aboutPOST /api/public/v1/events/:eventId/registrationsPOST /api/public/v1/join-applications
POST /api/auth/sign-in/email- sign in with
DEV_ADMIN_EMAILandDEV_ADMIN_PASSWORD
- sign in with
POST /api/internal/v1/publish-scheduled-content- send
Authorization: Bearer $INTERNAL_API_TOKENorx-internal-api-token
- send
After sign-in and cookie/session setup:
GET /api/admin/v1/meGET /api/admin/v1/dashboardGET /api/admin/v1/articlesGET /api/admin/v1/articles/referencesPOST /api/admin/v1/articlesPATCH /api/admin/v1/articles/:idPOST /api/admin/v1/articles/:id/publishGET /api/admin/v1/eventsGET /api/admin/v1/events/referencesPOST /api/admin/v1/eventsPATCH /api/admin/v1/events/:idPOST /api/admin/v1/events/:id/publishGET /api/admin/v1/events/:id/registrationsGET /api/admin/v1/registrations/:idPATCH /api/admin/v1/registrations/:idGET /api/admin/v1/applicationsGET /api/admin/v1/applications/:idPATCH /api/admin/v1/applications/:idGET /api/admin/v1/membersPOST /api/admin/v1/membersPATCH /api/admin/v1/members/:idGET /api/admin/v1/branchesPOST /api/admin/v1/branchesPATCH /api/admin/v1/branches/:idGET /api/admin/v1/assetsPOST /api/admin/v1/assets/uploadsPOST /api/admin/v1/assets/uploads/completeGET /api/admin/v1/staffPOST /api/admin/v1/staffPATCH /api/admin/v1/staff/:idGET /api/admin/v1/rolesPATCH /api/admin/v1/roles/:idGET /api/admin/v1/audit-logsGET /api/admin/v1/homepagePATCH /api/admin/v1/homepageGET /api/admin/v1/pages/joinPATCH /api/admin/v1/pages/joinGET /api/admin/v1/pages/aboutPATCH /api/admin/v1/pages/about
Recommended asset verification:
- upload one public image asset through the admin flow
- assign that asset as
coverAssetIdon one published article, event, branch, or member - confirm the matching public detail endpoint returns
coverImage.url
Recommended homepage/settings verification:
- update
homepagewith an active hero title and selected content IDs - confirm
GET /api/public/v1/homereflects the curated hero and section slices - update
pages/joinorpages/aboutand confirm the public page payload changes are reflected byGET /api/public/v1/joinorGET /api/public/v1/about
Recommended event registration verification:
- submit
POST /api/public/v1/events/spring-platform-workshop/registrationswithnameplus eitheremailorphoneNumber - confirm the submission appears in
GET /api/admin/v1/events/:id/registrations - open
GET /api/admin/v1/registrations/:idto verify the stored attendee payload - update the decision with
PATCH /api/admin/v1/registrations/:id - confirm
reviewedAtandreviewedByStaffIdare populated after the status change
Recommended audit verification:
- perform one protected mutation such as
PATCH /api/admin/v1/homepage - confirm
GET /api/admin/v1/audit-logsreturns a fresh entry withaction,targetType, actor identity, and before/after snapshots
Recommended staff and role verification:
- create one staff account with
POST /api/admin/v1/staff - update its
status,notes, or role assignments withPATCH /api/admin/v1/staff/:id - fetch
GET /api/admin/v1/staffagain to confirm the row reflects the new status and roles - call
GET /api/admin/v1/rolesto load the permission bundle catalog - save one role through
PATCH /api/admin/v1/roles/:idand confirm the response returns the updated permission set
Recommended scheduled publishing verification:
- create one article with
status=scheduledand ascheduledAtin the past - call
POST /api/internal/v1/publish-scheduled-contentwithINTERNAL_API_TOKEN - confirm the response lists the article under
published - confirm
GET /api/admin/v1/articles/:idnow returnsstatus=published - confirm
GET /api/admin/v1/audit-logscontainsarticle.publish_scheduled
Recommended rate-limit verification:
- temporarily lower
PUBLIC_APPLICATION_RATE_LIMIT_MAXorPUBLIC_EVENT_REGISTRATION_RATE_LIMIT_MAXin.env - restart
apps/api - submit the same public form repeatedly from one client
- confirm the API returns
429 RATE_LIMITEDwithRetry-After,X-RateLimit-Limit, andX-RateLimit-Remaining
- The public Astro site still falls back to shared demo content if the API is unavailable.
- Protected admin routes require both
DATABASE_URLandBETTER_AUTH_SECRET. - Object storage is only required when you want to exercise asset upload flows.
- Asset uploads use signed
PUTURLs, so the storage provider must allow browser CORS for direct uploads from the admin UI. - Local API smoke tests for uploads still work without browser CORS because the signed URL can be exercised from scripts or API clients.
- If
infra:up:storagefails while pulling container images, retry after Docker Hub connectivity recovers.