Development Guide
Development Guide
Section titled “Development Guide”Status: Authoritative development reference.
Cross-references: ARCHITECTURE.md (system overview), DEPLOYMENT.md (deployment setup), AGENTS.md (agent workflow).
Prerequisites
Section titled “Prerequisites”- Node.js 20+
- Go 1.24+ (for Go packages)
wranglerCLI (npm install -g wrangler)- Supabase CLI (
npm install -g supabase) — optional, for local migration testing - A running Supabase instance (cloud or local
supabase start)
Local development setup
Section titled “Local development setup”1. Clone and configure
Section titled “1. Clone and configure”git clone git@git.rbtr.dev:laputacloudco/gondolier.gitcd gondolier2. Set up secrets locally
Section titled “2. Set up secrets locally”Create a .dev.vars file in the project root:
# Development secretsSUPABASE_URL=http://localhost:54321SUPABASE_SERVICE_ROLE_KEY=dev-service-role-keyADMIN_KEY=dev-admin-keyGONDOLIER_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.
3. Start Supabase locally
Section titled “3. Start Supabase locally”# Start local Supabase (PostgreSQL + REST API)supabase start
# Or use an existing local PostgreSQL:export SUPABASE_URL=http://localhost:54321export SUPABASE_SERVICE_ROLE_KEY=<your-key>
# Run migrationssupabase db push4. Configure wrangler.toml for local
Section titled “4. Configure wrangler.toml for local”Update wrangler.toml with a local KV namespace:
[[kv_namespaces]]binding = "RATE_LIMIT_KV"id = "local" # placeholder, ignored in devFor local KV testing, use wrangler dev — it provides an in-memory KV
namespace automatically.
Running tests
Section titled “Running tests”Go tests
Section titled “Go tests”go vet ./...go test ./...go build ./cmd/gondolierJS tests
Section titled “JS tests”# Run all JS tests via wrangler's built-in test runnernpx wrangler test
# Or with Vitest (if configured separately)npx vitestRunning locally
Section titled “Running locally”Worker (wrangler dev)
Section titled “Worker (wrangler dev)”wrangler devThis starts the Worker on http://localhost:8787 with:
- In-memory KV namespace
- In-memory Durable Object storage
- Secrets loaded from
.dev.vars
API testing
Section titled “API testing”# Create a tenantcurl -X POST http://localhost:8787/api/v1/tenants \ -H "Content-Type: application/json" \ -d '{"name": "test-tenant", "api_key": "test-key-123"}'
# List tenantscurl http://localhost:8787/api/v1/tenants \ -H "X-API-Key: admin-key"
# Create connectioncurl -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"}'Database setup for migration testing
Section titled “Database setup for migration testing”Local PostgreSQL
Section titled “Local PostgreSQL”# Using Supabase CLIsupabase start
# Run migrations manuallypsql -h localhost -U postgres -d postgres -f migrations/001_base_tables.sqlpsql -h localhost -U postgres -d postgres -f migrations/002_user_accounts.sqlpsql -h localhost -U postgres -d postgres -f migrations/003_billing.sqlFresh database test
Section titled “Fresh database test”# Create a fresh database for migration testingcreatedb gondolier_testpsql -h localhost -U postgres -d gondolier_test -f migrations/001_base_tables.sql
# Verify tablespsql -h localhost -U postgres -d gondolier_test -c "\dt"Migration versioning
Section titled “Migration versioning”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.
Adding new endpoints
Section titled “Adding new endpoints”1. Define the route in handler.go
Section titled “1. Define the route in handler.go”// In SetupRoutes:mux.Handle("GET /api/v1/new-endpoint", auth)2. Implement the handler method
Section titled “2. Implement the handler method”func (h *Handler) GetNewEndpoint(w http.ResponseWriter, r *http.Request) { tenantID := r.Context().Value("tenant_id").(string) // ... implementation h.respondJSON(w, http.StatusOK, data)}3. Add to handleRequest switch
Section titled “3. Add to handleRequest switch”case strings.HasPrefix(path, "/api/v1/new-endpoint") && r.Method == "GET": h.GetNewEndpoint(w, r)4. Add tests
Section titled “4. Add tests”go test ./internal/api/... -run TestGetNewEndpointDebugging webhooks locally
Section titled “Debugging webhooks locally”Option 1: webhook.site
Section titled “Option 1: webhook.site”# 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 payloadsOption 2: ngrok
Section titled “Option 2: ngrok”# 1. Start wrangler devwrangler dev
# 2. Expose to internetngrok http 8787
# 3. Configure forge instance to push to:# https://<ngrok-id>.ngrok.io/api/v1/webhooks/forgejoOption 3: local forge instance
Section titled “Option 3: local forge instance”If you have a local Forgejo/Gitea instance:
[server]ROOT_URL = http://localhost:3000/
[webhook]ALLOWED_HOST_LIST = localhostLeaseDO local testing
Section titled “LeaseDO local testing”Using wrangler test
Section titled “Using wrangler test”# LeaseDO is tested via wrangler's durable object testing harnessnpx wrangler test --test-watchManual DO testing
Section titled “Manual DO testing”# Start wrangler devwrangler dev
# Test acquire/release via the DO directlycurl -X POST http://localhost:8787/lease -H "Content-Type: application/json"curl http://localhost:8787/leasecurl -X DELETE http://localhost:8787/lease -H "Content-Type: application/json"
# Test event processingcurl -X POST http://localhost:8787/event \ -H "Content-Type: application/json" \ -d '{"eventType":"pull_request","tenant":"test","repo":"owner/repo","action":"opened","prNumber":42}'Verifying DO state
Section titled “Verifying DO state”# Check queue statecurl http://localhost:8787/lease | python3 -m json.toolCode style and conventions
Section titled “Code style and conventions”See code-conventions.md for full details.
Go code
Section titled “Go code”- 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>.
JS/TS code
Section titled “JS/TS code”- No framework bloat (vanilla JS for Workers).
- Consistent error response format.
- Input validation on all API endpoints.
Adding a new migration
Section titled “Adding a new migration”-- migrations/004_new_feature.sqlBEGIN;
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"]Troubleshooting
Section titled “Troubleshooting”“Durable Object not found”
Section titled ““Durable Object not found””Ensure the DO binding in wrangler.toml matches the class name:
[durable_objects]bindings = [{ name = "LEASE_DO", class_name = "LeaseDO" }]“KV namespace not found”
Section titled ““KV namespace not found””Ensure the KV namespace ID in wrangler.toml matches the created namespace:
wrangler kv:namespace list“Migration failed”
Section titled ““Migration failed””Check the migration SQL in a local PostgreSQL instance:
psql -h localhost -U postgres -d postgres -f migrations/001_base_tables.sql“Webhook HMAC verification failed”
Section titled ““Webhook HMAC verification failed””Verify the webhook secret matches:
# In Supabase:SELECT webhook_secret FROM forge_connections WHERE instance_url = 'https://git.example.com';
# On the forge instance:# Settings → Webhooks → Check secret“Rate limit exceeded”
Section titled ““Rate limit exceeded””The KV-based rate limiter uses a sliding window. Wait for the window to expire (or increase the window size in the limiter config).
Project structure reference
Section titled “Project structure reference”index.mjs ← Worker entry point, routing, LeaseDO exportwrangler.toml ← Workers config
migrations/ ← Supabase migration SQLpkg/crypto/ ← Envelope encryptioninternal/api/ ← REST API handler + routerinternal/tenant/ ← Tenant data modelsinternal/db/ ← Database access layerinternal/scheduler/ ← Engine schedulerinternal/lease/ ← Lease management clientinternal/cron/ ← Cron reconciliationinternal/engine/ ← Shunt engine integrationinternal/forge/ ← Workers-compatible forge clientinternal/gitops/ ← Staging branch managementsite/ ← Sell site (Cloudflare Pages)templates/ ← Dashboard templatestests/ ← Integration tests