- Go 1.23+
- Rancher Desktop (or Docker Desktop) — for local Postgres + Redis
psqlclient — for running migrations manuallykubectl— for k8s deployment (optional)
# Clone and enter the repo
git clone https://github.com/InstaNode-dev/api
cd api
# Generate cryptographic secrets for local dev
make gen-secrets
# Copy the output into .env
cp .env.example .env
# Fill in JWT_SECRET and AES_KEY from gen-secrets output.
# All other vars are optional in development.
# Start the database containers
make docker-up
# Run platform schema migrations
make migrate-platform
# Start the server (hot reload not included — just re-run on changes)
make runThe server starts on :8080. Verify with curl http://localhost:8080/healthz.
Tests split into two tiers:
Run against real Postgres and Redis. Set TEST_DATABASE_URL and TEST_REDIS_URL, or rely on defaults:
# Defaults: postgres://postgres:postgres@localhost:5432/instant_dev_test, redis://localhost:6379/15
make test
# With explicit DSNs
TEST_DATABASE_URL=postgres://instant:instant@localhost:5432/instant_platform?sslmode=disable \
TEST_REDIS_URL=redis://localhost:6379/15 \
make testRun against a live server — uses real HTTP, no in-process Fiber:
# Against local k8s (Rancher Desktop)
make test-e2e # default: http://localhost:32108
# Against docker-compose
make docker-up && make migrate-platform
make run &
make test-e2e-docker # http://localhost:8080E2E tests use the //go:build e2e build tag and live in e2e/. They are isolated from the regular test suite and will not run with go test ./....
internal/config/ — Environment loading. Add new env vars here only.
internal/crypto/ — AES encryption, JWT signing, IP fingerprinting. No external state.
internal/db/ — DB + Redis connection helpers. One function per resource.
internal/email/ — Resend client. Wrap all sends in the Client.send() dispatcher.
internal/handlers/ — HTTP handlers. Each handler file owns one domain (ping, auth, etc.).
internal/jobs/ — River background workers. One worker per file.
internal/metrics/ — Prometheus gauges/counters/histograms. Register once at package init.
internal/middleware/ — Fiber middleware. Each file is one middleware function.
internal/models/ — DB query functions. No ORM — raw sql.DB + typed errors only.
internal/router/ — Fiber app assembly. Middleware chain order is load-bearing.
internal/testhelpers/ — Shared test utilities. No production code here.
e2e/ — Black-box HTTP tests. Must compile with -tags e2e.
No ORM. All DB queries use database/sql directly. Write typed error sentinel types (ErrXNotFound, ErrXAlreadyUsed) rather than checking sql.ErrNoRows in handlers.
Fail-open on Redis errors. Rate limit and cache misses must never block a request. Log the error and continue.
Two separate databases. Platform metadata (teams, users, resources, pings) goes in DATABASE_URL. Customer-provisioned databases (Phase 2+) go in CUSTOMER_DATABASE_URL. Never mix them.
Atomic single-use enforcement. The onboarding JWT claim uses an atomic UPDATE ... WHERE jti = $2 AND converted_at IS NULL RETURNING id. All single-use semantics must use this pattern — never a SELECT-then-UPDATE.
Typed errors over sentinel strings. Define type ErrFoo struct { ... } and use errors.As in callers. Do not check err.Error() == "some string".
Middleware context values. Use the typed getters in internal/middleware (GetFingerprint, GetGeoCountry, etc.) to read context values. Do not access fiber.Locals with string keys directly in handlers.
Prometheus metrics. Every new significant codepath needs at least one counter. Register metrics in internal/metrics/metrics.go. Label cardinality must stay low — no per-token labels.
Background jobs. All scheduled work runs as River workers. Do not add time.Sleep loops or cron.Cron schedulers. See internal/jobs/workers.go for registration.
Feature flags. Gate new services behind cfg.IsServiceEnabled("service-name"). Add the service name to INSTANT_ENABLED_SERVICES in .env.example when ready to enable by default.
- Add the handler method to the appropriate file in
internal/handlers/. - Register the route in
internal/router/router.go. - Add model functions in
internal/models/if new DB queries are needed. - Add integration tests in
internal/handlers/*_test.go. - Add E2E coverage in
e2e/e2e_test.go.
- Create
internal/jobs/{name}.gowith a River worker struct. - Register it in
internal/jobs/workers.go(workers.AddWorker). - Schedule it in
StartWorkerswithriver.NewClientperiodic jobs.
Add a new migration file:
# Name it sequentially
touch internal/db/migrations/002_add_something.sql
# Apply locally
make migrate-platform
# Embed in the k8s ConfigMap
make k8s-regen-migrationsAll migration SQL must be idempotent (CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS).
If adding a new monthly pings partition, update the helpers in testhelpers.go too.
-
go build ./...passes -
go vet ./...passes - New code has unit or integration tests
- E2E tests updated if new endpoints added
-
.env.exampleupdated if new env vars added - No secrets or credentials committed
- Migrations are idempotent