Skip to content

Development Guide

Status: Authoritative development reference.

Cross-references: ARCHITECTURE.md (system overview), DEPLOYMENT.md (deployment setup), AGENTS.md (agent workflow).

  • Node.js 20+
  • Go 1.24+ (for Go packages)
  • wrangler CLI (npm install -g wrangler)
  • Supabase CLI (npm install -g supabase) — optional, for local migration testing
  • A running Supabase instance (cloud or local supabase start)
Terminal window
git clone git@git.rbtr.dev:laputacloudco/gondolier.git
cd gondolier

Create a .dev.vars file in the project root:

Terminal window
# Development secrets
SUPABASE_URL=http://localhost:54321
SUPABASE_SERVICE_ROLE_KEY=dev-service-role-key
ADMIN_KEY=dev-admin-key
GONDOLIER_MASTER_KEY=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))")

The master key must be exactly 32 bytes (base64-encoded = 44 chars). Use openssl rand -base64 32 to generate one.

Terminal window
# Start local Supabase (PostgreSQL + REST API)
supabase start
# Or use an existing local PostgreSQL:
export SUPABASE_URL=http://localhost:54321
export SUPABASE_SERVICE_ROLE_KEY=<your-key>
# Run migrations
supabase db push

Update wrangler.toml with a local KV namespace:

[[kv_namespaces]]
binding = "RATE_LIMIT_KV"
id = "local" # placeholder, ignored in dev

For local KV testing, use wrangler dev — it provides an in-memory KV namespace automatically.

Terminal window
go vet ./...
go test ./...
go build ./cmd/gondolier
Terminal window
# Run all JS tests via wrangler's built-in test runner
npx wrangler test
# Or with Vitest (if configured separately)
npx vitest
Terminal window
wrangler dev

This starts the Worker on http://localhost:8787 with:

  • In-memory KV namespace
  • In-memory Durable Object storage
  • Secrets loaded from .dev.vars
Terminal window
# Create a tenant
curl -X POST http://localhost:8787/api/v1/tenants \
-H "Content-Type: application/json" \
-d '{"name": "test-tenant", "api_key": "test-key-123"}'
# List tenants
curl http://localhost:8787/api/v1/tenants \
-H "X-API-Key: admin-key"
# Create connection
curl -X POST http://localhost:8787/api/v1/tenants/:id/connections \
-H "X-API-Key: test-key-123" \
-H "Content-Type: application/json" \
-d '{"instance_url": "https://git.example.com", "token": "ghp_xxx", "token_type": "pat"}'
Terminal window
# Using Supabase CLI
supabase start
# Run migrations manually
psql -h localhost -U postgres -d postgres -f migrations/001_base_tables.sql
psql -h localhost -U postgres -d postgres -f migrations/002_user_accounts.sql
psql -h localhost -U postgres -d postgres -f migrations/003_billing.sql
Terminal window
# Create a fresh database for migration testing
createdb gondolier_test
psql -h localhost -U postgres -d gondolier_test -f migrations/001_base_tables.sql
# Verify tables
psql -h localhost -U postgres -d gondolier_test -c "\dt"

Migrations are versioned and stored in migrations/ and supabase/migrations/:

File Purpose
001_base_tables.sql tenants, forge_connections, managed_repos, audit_log
002_user_accounts.sql user accounts, organizations
003_billing.sql billing, subscriptions

New migrations should be numbered sequentially and be forward-compatible — new code must handle both old and new schema.

// In SetupRoutes:
mux.Handle("GET /api/v1/new-endpoint", auth)
func (h *Handler) GetNewEndpoint(w http.ResponseWriter, r *http.Request) {
tenantID := r.Context().Value("tenant_id").(string)
// ... implementation
h.respondJSON(w, http.StatusOK, data)
}
case strings.HasPrefix(path, "/api/v1/new-endpoint") && r.Method == "GET":
h.GetNewEndpoint(w, r)
Terminal window
go test ./internal/api/... -run TestGetNewEndpoint
Terminal window
# 1. Create a test endpoint at webhook.site
# 2. Configure your local forge instance to push to:
# https://webhook.site/<your-uuid>/api/v1/webhooks/forgejo
# 3. Check webhook.site for received payloads
Terminal window
# 1. Start wrangler dev
wrangler dev
# 2. Expose to internet
ngrok http 8787
# 3. Configure forge instance to push to:
# https://<ngrok-id>.ngrok.io/api/v1/webhooks/forgejo

If you have a local Forgejo/Gitea instance:

settings/app.ini
[server]
ROOT_URL = http://localhost:3000/
[webhook]
ALLOWED_HOST_LIST = localhost
Terminal window
# LeaseDO is tested via wrangler's durable object testing harness
npx wrangler test --test-watch
Terminal window
# Start wrangler dev
wrangler dev
# Test acquire/release via the DO directly
curl -X POST http://localhost:8787/lease -H "Content-Type: application/json"
curl http://localhost:8787/lease
curl -X DELETE http://localhost:8787/lease -H "Content-Type: application/json"
# Test event processing
curl -X POST http://localhost:8787/event \
-H "Content-Type: application/json" \
-d '{"eventType":"pull_request","tenant":"test","repo":"owner/repo","action":"opened","prNumber":42}'
Terminal window
# Check queue state
curl http://localhost:8787/lease | python3 -m json.tool

See code-conventions.md for full details.

  • Table-driven tests only.
  • Explicit error handling (no swallowed errors).
  • Structured logging with tenant_id, repo, action, duration_ms.
  • Never log raw tokens — use <REDACTED>.
  • No framework bloat (vanilla JS for Workers).
  • Consistent error response format.
  • Input validation on all API endpoints.
-- migrations/004_new_feature.sql
BEGIN;
CREATE TABLE IF NOT EXISTS new_feature (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_new_feature_tenant ON new_feature(tenant_id);
COMMIT;

Then update the migration tag in wrangler.toml:

[[migrations]]
tag = "v2"
new_sqlite_classes = ["LeaseDO"]

Ensure the DO binding in wrangler.toml matches the class name:

[durable_objects]
bindings = [{ name = "LEASE_DO", class_name = "LeaseDO" }]

Ensure the KV namespace ID in wrangler.toml matches the created namespace:

Terminal window
wrangler kv:namespace list

Check the migration SQL in a local PostgreSQL instance:

Terminal window
psql -h localhost -U postgres -d postgres -f migrations/001_base_tables.sql

Verify the webhook secret matches:

Terminal window
# In Supabase:
SELECT webhook_secret FROM forge_connections WHERE instance_url = 'https://git.example.com';
# On the forge instance:
# Settings → Webhooks → Check secret

The KV-based rate limiter uses a sliding window. Wait for the window to expire (or increase the window size in the limiter config).

index.mjs ← Worker entry point, routing, LeaseDO export
wrangler.toml ← Workers config
migrations/ ← Supabase migration SQL
pkg/crypto/ ← Envelope encryption
internal/api/ ← REST API handler + router
internal/tenant/ ← Tenant data models
internal/db/ ← Database access layer
internal/scheduler/ ← Engine scheduler
internal/lease/ ← Lease management client
internal/cron/ ← Cron reconciliation
internal/engine/ ← Shunt engine integration
internal/forge/ ← Workers-compatible forge client
internal/gitops/ ← Staging branch management
site/ ← Sell site (Cloudflare Pages)
templates/ ← Dashboard templates
tests/ ← Integration tests