Project Structure
A scaffolded project
gkm init lays out a single-app project like this:
my-api/
├── gkm.config.ts # constructs glob, routes glob, backends
├── src/
│ ├── constructs/ # what this app declares
│ │ ├── database.ts # KyselyDatabase — why a Postgres exists
│ │ ├── storage.ts # ObjectStorage — why a MinIO exists
│ │ ├── cache.ts # Cache
│ │ └── email.ts # Email — why a Mailpit exists
│ ├── endpoints/ # the routes glob
│ │ ├── health.ts
│ │ └── users/
│ ├── router.ts # the shared endpoint factory
│ └── config/
│ ├── env.ts # EnvironmentParser + Credentials
│ └── logger.ts
├── db/
│ └── database/ # one folder per database construct
│ ├── migrations/ # the schema's history — gkm migrate
│ └── seeds/ # reference data, run every pass — gkm seed
├── test/ # transaction-isolated suite + factories
└── .gkm/ # generated — compose, secrets, manifestTwo directories carry the model:
src/constructs/— one file per declaration. Everything else derives from what is here: the containersgkm devstarts, the env keys the build writes, and the cloud resources a deploy provisions.src/endpoints/— the handlers, which reach those declarations by edge (.dependsOn([…])) rather than by env key.
.gkm/ is generated and gitignored except for secrets/, which is encrypted and safe to commit.
One glob, every kind
constructs: './src/constructs/**/*.ts' is a convention, not a requirement — discovery inspects every export of every matching module and keeps the ones with an id that can declare(). Point it at ./src/**/*.ts and colocate a bucket with the feature that uses it if you prefer.
An under-inclusive glob silently drops declarations
The glob is not a filter over things gkm already knows about — it is how they are found at all. A file it does not match is never imported, so its declarations do not exist: no container, no env key, no deployed resource, and no error saying so.
*.ts matches one level; **/*.ts matches any depth. Moving a declaration into a subdirectory is enough to lose it. Widening the glob costs startup time and nothing else, so prefer the wider one.
A workspace
my-saas/
├── gkm.config.ts # defineWorkspace — apps, backends, deploy
├── apps/
│ ├── api/
│ │ ├── src/constructs/ # this app's declarations
│ │ └── src/endpoints/
│ ├── auth/
│ └── web/
└── packages/
├── models/ # shared schemas
└── ui/Each app's constructs glob resolves against that app's own path, and reconcile reads all of them into one manifest. Apps share infrastructure by declaring derived forms of it — database.schema() for a schema of its own, database.reader() for read-only access — never by agreeing on a URL string.
The toolbox repo itself
toolbox/
├── packages/
│ ├── constructs/ # endpoints, functions, crons, and the resource constructs
│ ├── manifest/ # declarations, naming, derivation — the build/run seam
│ ├── cli/ # gkm: reconcile, dev, build, deploy
│ ├── cloud/ # deploy targets
│ └── … # audit, auth, cache, db, envkit, errors, events, …
├── apps/
│ ├── docs/ # this site
│ └── example/ # a worked v10 app
├── docs/design/ # the design docs, including the constructs paradigm
└── turbo.jsonBuilt with tsdown (ESM + CJS), orchestrated by Turbo, linted and formatted by Biome.
pnpm install
pnpm build
pnpm test
pnpm lint