Skip to content

How the system fits together

One Bun workspace, three deployed services, one Postgres database.

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.

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/*, no server-only, no bun-types
  • @sentry/core, never @sentry/nextjs
  • revalidation goes through packages/core/src/lib/revalidate.ts (which calls the web /api/revalidate route), never next/cache

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/upload

Everything 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.

  1. tRPC — what the web app calls. Routers live in packages/core/src/server/trpc/routers/.
  2. REST /v1 — the mobile app’s surface, in apps/api/src/v1/routes/. Handlers are adapters over the same tRPC routers via createCaller, so a business rule is implemented once. OpenAPI at /v1/openapi.json, browsable docs at /v1/docs.
  3. 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.tsbayarcash_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.

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.