backend-server-anatomy

Canonical folder structure and architecture pattern for gRPC backend servers under apps/servers/. No concrete reference implementation currently exists on disk — this is the pattern to follow when sca

SKILL.md

GitHub

Backend gRPC Server Anatomy

No concrete reference implementation currently exists in apps/servers/ — the previous examples (server1-grpc, basic CRUD; server2-grpc, adds a Debezium CDC consumer + Meilisearch indexing) were removed. This doc captures the pattern they established; treat it as the spec for the next server scaffolded, and update this doc to point at it once one exists.

Folder structure

apps/servers/{name}-grpc/
├── Dockerfile
├── README.md
├── .env.sample
├── helm/
│   ├── Chart.yaml
│   ├── values.yaml
│   ├── values-prod.yaml
│   └── templates/
│       ├── deployment.yaml
│       ├── env.yaml
│       └── redis.yaml          ← only if redis extension installed (Deployment/Service/Secret, same chart)
├── index.ts                    ← entry point, calls src/app.ts
├── package.json                ← scripts: dev, gen, postinstall (prisma generate, if DB)
├── prisma.config.ts            ← only present when the database extension is installed
├── tsconfig.json
├── generated/prisma/           ← Prisma client output. NEVER edit, regenerated by `prisma generate`
└── src/
    ├── app.ts                  ← ServerApp composition root (see below)
    ├── topics.ts                ← Kafka topic name constants
    ├── configs/
    │   └── proto/buf.gen.yaml  ← buf codegen config (present when gRPC driver installed)
    ├── schemas/
    │   ├── prisma/schema.prisma + migrations/   ← source of truth for DB schema
    │   └── proto/{service}.proto                ← source of truth for gRPC contract
    ├── repositories/
    │   └── {Entity}Repository.ts   ← extends BaseRepository<PrismaClient>
    ├── routers/
    │   ├── index.ts
    │   ├── {Entity}GrpcRouter.ts       ← extends GrpcRouter<...ServiceServer>
    │   └── {Entity}DebeziumRouter.ts   ← extends KafkaConsumerRouter (only if consuming CDC)
    └── usecases/
        ├── index.ts
        ├── Create{Entity}UseCase.ts
        ├── Get{Entity}UseCase.ts
        ├── List{Entity}UseCase.ts
        ├── Update{Entity}UseCase.ts
        └── Delete{Entity}UseCase.ts

Architecture axioms

  • src/schemas/proto/{service}.proto is the source of truth for the gRPC contract. Edit the .proto, then regenerate — never hand-edit generated stubs (see code-generation-pipeline skill).
  • src/app.ts composes a ServerApp (from the server package) via a fluent builder: drivers (GrpcDriver, optionally KafkaDriver for producing), .database(PrismaClient, new PgAdapter(databaseUrl)) if the DB extension is installed, .plugins([...]) (HealthCheckPlugin, RedisPlugin, OtelPlugin were the common set in the previous reference servers — add more as needed), .containers({...}) to register repositories into the DI container (transient(...) per request, singleton(...) shared), .routers([...]).
  • Router = wiring, not logic. A GrpcRouter subclass only maps RPC method names to UseCase classes via get handlers() — it holds no business logic itself.
  • UseCase pattern: one class per operation (one verb, one entity). Constructor receives its repository (and any plugin clients it needs, e.g. Kafka producer) through the DI container — it does not reach into globals.
  • Repository pattern: one class per entity, extends BaseRepository<PrismaClient>. Owns all Prisma access for that entity plus any cache-aside logic (cacheAside/cacheAsideAll helpers from server package) and any event publishing it does directly (e.g. producing a Kafka event on create).
  • CDC consumer router: when the server consumes Debezium change events (not just produces), the router extends KafkaConsumerRouter<TopicTypes>, not the producer-side KafkaDriver config. It decodes Debezium's JSON envelope (not a Schema Registry protobuf schema) and dispatches to a UseCase per topic.
  • Servers with no auth requirement don't need to wire an auth interceptor — the previous reference servers ran without one. Don't invent one unless the task requires it.

Companion infra chart pattern

A server with the database extension gets a sibling apps/servers/{name}-infra chart (Postgres only — Redis lives directly in the app chart itself, see the folder structure above), Terraform- applied — see root CLAUDE.md's "Per-server infra" section for why.

apps/servers/{name}-infra/helm/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── db.yaml                 ← PostgreSQL Deployment/Service/Secret, static superuser password
    └── debezium.yaml           ← only if debezium extension installed

The app chart's own env.yaml reads that static password cross-namespace via a Helm lookup at render time (no provisioning Job, no rotation — same static-credential pattern Redis uses too, just without the cross-namespace step since Redis's Deployment/Secret share the app chart's own namespace). A per-deploy Vault-minted dynamic credential was evaluated and dropped in favor of this — see root CLAUDE.md's services/* note on why Vault (and Infisical, evaluated as a replacement) were both removed from this repo.

All -infra charts land in the one shared server-infra namespace (see apps/terraform/main.tf); collisions are avoided by prefixing every object name with the server's own name, not by a per-server namespace.

Scaffolding — never build this by hand

bun run generate
# select: server
# follow prompts to add drivers (grpc, graphql, kafka, cron) and extensions (database, redis, debezium, secrets)

This scaffolds the full structure above, including helm/.