How the system fits together
One Bun workspace, three deployed services, one Postgres database.
Packages
Section titled “Packages”| Package | What is in it | Imported as |
|---|---|---|
apps/web |
Next.js App Router. Pages, components, hooks, client stores only. | @/... |
apps/api |
Bun + Hono. Mounts tRPC, Better Auth, the mobile REST /v1 surface, webhooks, cron, uploads. |
— |
packages/core |
Every piece of server logic: tRPC routers, services, audit, auth, email, Stripe, RBAC, cron, HTTP handlers. | @tcgkl/core/<path> |
packages/db |
Drizzle schema, migrations, the DB client, and pure constants the schema imports. | @tcgkl/db, @tcgkl/db/schema |
apps/docs |
This site. | — |
apps/whatsapp-gateway |
Baileys WhatsApp session. Deliberately not a workspace member — own manifest, own image. | — |
packages/core is runtime-agnostic
Section titled “packages/core is runtime-agnostic”It runs under Bun in apps/api and under Node in apps/web, so it must not
import anything Next- or Bun-specific:
- no
next/*, noserver-only, nobun-types @sentry/core, never@sentry/nextjs- revalidation goes through
packages/core/src/lib/revalidate.ts(which calls the web/api/revalidateroute), nevernext/cache
How a browser request reaches the API
Section titled “How a browser request reaches the API”The browser only ever talks to the web origin, so cookies and CORS never change.
apps/web/src/proxy.ts rewrites the API-owned paths when API_INTERNAL_URL is
set:
/api/trpc/* /api/create-payment-intent/api/auth/* /api/upload/api/webhooks/* /api/profile/avatar/api/cron/*/api/admin/uploadEverything else under /api/* is Next-only. /api/auth/callback is an explicit
exception — a legacy Supabase stub that stays local.
With API_INTERNAL_URL unset, the local thin routes serve instead. That is the
dev-without-the-API path and the rollback path, in one switch.
The three API surfaces
Section titled “The three API surfaces”- tRPC — what the web app calls. Routers live in
packages/core/src/server/trpc/routers/. - REST
/v1— the mobile app’s surface, inapps/api/src/v1/routes/. Handlers are adapters over the same tRPC routers viacreateCaller, so a business rule is implemented once. OpenAPI at/v1/openapi.json, browsable docs at/v1/docs. - Webhooks and cron — Stripe, WhatsApp, Coolify cron, served from
packages/core/src/http/.
Every table lives in the web Postgres schema on NeonDB, declared with
webSchema.table(). Two tables are production-only and absent from
schema.ts — bayarcash_transactions and stripe_charges_imported. Treat them
as read-only.
Redis lives with the API only. The web app has no REDIS_URL, so every
web-side Redis consumer needs a null or database fallback.
Deployed shape
Section titled “Deployed shape”Three images are built on every push (web, API, WhatsApp gateway); deploys are
gated on which paths changed. main deploys production, beta deploys staging.
Details in Deploys.
