Skip to content

Getting Started

Prerequisites

  • Node.js >= 22.0.0
  • pnpm >= 10.13.1
  • Docker (for local services — PostgreSQL, Redis, MinIO, etc.)

Install the CLI

bash
pnpm add -g @geekmidas/cli

Create a New Project

bash
gkm init my-project

Interactive Setup

The init command walks you through project setup:

? Template:
  ❯ API        — Single backend API with endpoints
    Fullstack  — Monorepo with API + Next.js + shared models

? Services (space to select):
  ◉ PostgreSQL
  ◉ Redis
  ◉ Mailpit
  ◉ MinIO

? Event backend:
  ❯ pg-boss   — PostgreSQL job queue (no extra container)
    SNS/SQS   — AWS via LocalStack
    RabbitMQ  — AMQP broker
    None

? Deployment target:
  ❯ Dokploy
    Configure later

Quick Start (non-interactive)

bash
# All defaults: fullstack template, all services, pgboss, dokploy
gkm init my-saas --template fullstack --yes

# API-only
gkm init my-api --template api --yes

Templates

TemplateDescription
apiSingle backend API with endpoints, auth, database
fullstackMonorepo — API + Better Auth + Next.js + shared models
workerBackground job processing with events, crons, subscribers
serverlessAWS Lambda handlers
minimalHealth endpoint only

INFO

The interactive prompt shows API and Fullstack by default. Use --template to select others:

bash
gkm init my-worker --template worker --yes

End-to-End: From Init to Running App

This section walks through everything after gkm init — how credentials work, how services start, and how your app receives injected environment variables.

1. What gkm init Creates

After init, your project has encrypted secrets already bootstrapped for the development stage:

my-project/
├── .gkm/
│   └── secrets/
│       └── development.json   # encrypted credentials (safe to commit)
├── gkm.config.ts              # workspace / app config
├── docker-compose.yml         # local services
└── ...

The key for decrypting development.json is stored separately at:

~/.gkm/my-project/development.key

This key never enters source control. The encrypted secrets file can be committed freely.

2. What Gets Seeded Automatically

gkm init generates and encrypts sane development defaults including:

SecretValue
DATABASE_URLpostgres://...@localhost:5432/my-project
REDIS_URLredis://localhost:6379
STORAGE_ACCESS_KEY_IDderived from MinIO default user
STORAGE_SECRET_ACCESS_KEYderived from MinIO default password
EVENT_PUBLISHER_CONNECTION_STRINGpgboss connection string
EVENT_SUBSCRIBER_CONNECTION_STRINGpgboss connection string
JWT_SECRETrandomly generated
NODE_ENVdevelopment

You can view them any time:

bash
gkm secrets:show --stage development

3. Reconcile Services (First Run)

Run gkm setup once after init (and after changing services in the config). It:

  • Starts Docker services with credentials injected from your stage secrets — each project gets its own isolated containers, so multiple projects can run concurrently without port or credential collisions
  • Creates the database, roles, and schemas
  • Sets up pgboss if events: 'pgboss'
  • Validates that all required secrets exist
bash
gkm setup

4. Start the Dev Server

bash
gkm dev

gkm dev decrypts your stage secrets and injects them as environment variables before starting each app and its Docker services. Your code accesses them via process.env — no .env files needed. Because secrets are injected dynamically, multiple projects can run simultaneously without collisions.

[api]  Decrypting secrets (development)...
[api]  Starting on http://localhost:3000
[api]  Telescope: http://localhost:3000/__telescope
[auth] Starting on http://localhost:3002
[web]  Waiting for api, auth...
[web]  Starting on http://localhost:3001

Multiple projects

gkm dev and gkm test each spin up Docker services with project-scoped credentials. Running gkm dev in two different projects starts two independent PostgreSQL (and Redis, MinIO, etc.) containers — no manual port mapping needed.

5. How Credential Injection Works

gkm dev (and gkm exec) use a preload script to inject secrets before your app code runs. The flow is:

~/.gkm/my-project/development.key

         ▼ decrypt
.gkm/secrets/development.json

         ▼ AES-256-GCM
globalThis.__gkm_credentials__  (set by preload)

         ▼ read by
@geekmidas/envkit Credentials

         ▼ merged into
EnvironmentParser.parse()

In your app code:

typescript
// apps/api/src/config/env.ts
import { EnvironmentParser } from '@geekmidas/envkit';
import { Credentials } from '@geekmidas/envkit/credentials';

export const envParser = new EnvironmentParser({
  ...process.env,
  ...Credentials,  // ← injected secrets merged here
});

export const config = envParser
  .create((get) => ({
    port: get('PORT').string().transform(Number).default(3000),
    databaseUrl: get('DATABASE_URL').string().url(),
    redisUrl: get('REDIS_URL').string(),
  }))
  .parse();

Credentials resolves (in priority order):

  1. globalThis.__gkm_credentials__ — set by gkm dev/gkm exec preload
  2. Build-time decryption via GKM_MASTER_KEY — for CI/CD and Docker builds
  3. Empty object — fallback when no credentials are available

WARNING

Never read secrets with process.env directly in production-critical paths. Always go through EnvironmentParser with Credentials merged in — this ensures the same code works in dev (preload injection) and production (build-time or runtime decryption).

6. Running One-Off Commands with Secrets

Use gkm exec to run any command with secrets injected:

bash
# Run a migration with DATABASE_URL injected
gkm exec -- pnpm db:migrate

# Run a script with all dev secrets
gkm exec -- node scripts/seed.ts

# Open a psql shell with DATABASE_URL injected
gkm exec -- psql $DATABASE_URL

7. Managing Secrets

bash
# Add a new secret
gkm secrets:set STRIPE_KEY sk_test_xxx --stage development

# View all secrets (values masked)
gkm secrets:show --stage development

# Rotate the encryption key
gkm secrets:rotate --stage development

# Initialize a new stage (e.g., production)
gkm secrets:init --stage production

Production secrets use the same encrypted format but get a separate key at ~/.gkm/my-project/production.key. In CI/CD, provide the key via GKM_MASTER_KEY.


Workspace Configuration

The generated gkm.config.ts for the fullstack template:

typescript
import { defineWorkspace } from '@geekmidas/cli/config';

export default defineWorkspace({
  name: 'my-saas',

  apps: {
    api: {
      type: 'backend',
      path: 'apps/api',
      port: 3000,
      routes: './src/endpoints/**/*.ts',
      envParser: './src/config/env#envParser',
      logger: './src/config/logger#logger',
      openapi: { enabled: true },
    },
    auth: {
      type: 'backend',
      path: 'apps/auth',
      port: 3002,
      entry: './src/index.ts',
      framework: 'better-auth',
      envParser: './src/config/env#envParser',
      logger: './src/config/logger#logger',
    },
    web: {
      type: 'frontend',
      framework: 'nextjs',
      path: 'apps/web',
      port: 3001,
      dependencies: ['api', 'auth'],
    },
  },

  shared: {
    packages: ['packages/*'],
    models: { path: 'packages/models', schema: 'zod' },
  },

  services: {
    db: true,       // postgres:18-alpine
    cache: true,    // redis:8-alpine
    mail: true,     // Mailpit in dev, SMTP in production
    storage: true,  // MinIO in dev, AWS S3 in production
    events: 'pgboss', // pgboss | sns | rabbitmq
  },

  secrets: { enabled: true },
});

Services Reference

KeyDev containerProduction
db: truepostgres:18-alpineProvisioned PostgreSQL
cache: trueredis:8-alpineProvisioned Redis
mail: trueMailpitSMTP provider
storage: trueMinIOAWS S3
events: 'pgboss'Reuses PostgreSQLReuses PostgreSQL
events: 'sns'LocalStack (SNS+SQS)AWS SNS+SQS
events: 'rabbitmq'RabbitMQProvisioned RabbitMQ

Create Your First Endpoint

typescript
// apps/api/src/endpoints/users.ts
import { e } from '@geekmidas/constructs/endpoints';
import { z } from 'zod';

export const listUsers = e
  .get('/users')
  .output(z.array(z.object({
    id: z.string(),
    name: z.string(),
    email: z.string(),
  })))
  .handle(async () => {
    return [
      { id: '1', name: 'Alice', email: 'alice@example.com' },
    ];
  });

The endpoint is auto-discovered from the routes glob and available at GET /users.

Inject Services into Endpoints

typescript
// apps/api/src/services/database.ts
import type { Service } from '@geekmidas/services';
import { Kysely, PostgresDialect } from 'kysely';
import pg from 'pg';

export const databaseService = {
  serviceName: 'db' as const,
  async register(envParser) {
    const { databaseUrl } = envParser
      .create((get) => ({ databaseUrl: get('DATABASE_URL').string() }))
      .parse();
    return new Kysely({
      dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: databaseUrl }) }),
    });
  },
} satisfies Service<'db', Kysely<Database>>;

// apps/api/src/endpoints/users.ts
export const listUsers = e
  .get('/users')
  .services([databaseService])
  .handle(async ({ services }) => {
    return await services.db.selectFrom('users').selectAll().execute();
  });

Build for Production

bash
# Build server application
gkm build --provider server --production

# Generate Docker files
gkm docker

# Deploy (Dokploy)
gkm deploy --stage production

Next Steps