Thank you for your interest in contributing! This document covers everything you need to get a development environment running, write and run tests, follow code conventions, and submit a pull request.
- Bun (latest)
- Docker (used to run PostgreSQL and ClickHouse locally)
-
Fork and clone
git clone https://github.com/<your-fork>/Scrawn.git cd Scrawn
-
Install dependencies
bun install
-
Configure environment
cp .env.example .env.local
Fill in
.env.local— at minimum you needDATABASE_URL,HMAC_SECRET,DODO_PAYMENTS_LIVE_API_KEY,DODO_PAYMENTS_TEST_API_KEY,DODO_PAYMENTS_LIVE_PRODUCT_ID,DODO_PAYMENTS_TEST_PRODUCT_ID, andDODO_PAYMENTS_WEBHOOK_SECRET. For ClickHouse development also addCLICKHOUSE_URLand setSTORAGE_ADAPTER=clickhouse. -
Start infrastructure
docker compose up -d
-
Run migrations
# Postgres (always required) bunx drizzle-kit push # ClickHouse (only if STORAGE_ADAPTER=clickhouse) bun run migrate:clickhouse
-
Start the dev server
bun run dev:backend
This starts the gRPC server on
:8069and the Fastify HTTP server on:8070with auto-reload.
src/
config/ — env parsing and constants
errors/ — typed error classes and the WideEventLogger
gen/ — generated protobuf types (do not edit by hand)
interceptors/ — gRPC server interceptors (auth, logging)
routes/
gRPC/ — gRPC service implementations
http/ — Fastify route handlers (webhooks, API)
servers/ — gRPC and Fastify server bootstrap
storage/
adapter/ — PostgresAdapter and ClickHouseAdapter
db/
postgres/ — Drizzle schema + DB singleton
clickhouse/ — ClickHouse client singleton + migrations
utils/ — shared utilities (hashing, API key generation, etc.)
zod/ — Zod schemas for request validation
__tests__/
fixtures/ — test data factories (API keys, gRPC clients)
db/ — storage-adapter-agnostic test DB interface
assertions/ — reusable assertion helpers per domain
proto/ — protobuf definitions (git submodule)
drizzle/ — Drizzle migration files
We use TypeScript in strict mode with Bun as the runtime. Run bun run typecheck and bun run format before committing — these are enforced by Husky pre-commit hooks.
- Imports — use
import typefor type-only imports - Types — always type function parameters and return values; avoid
any; don't cast what can be inferred - Error handling — use custom error classes (
AuthError,StorageError, etc.) with static factory methods; always includetype,message, and optionaloriginalError - Validation — use Zod schemas for all incoming request data; catch
ZodErrorand convert to domain errors - Logging — use
WideEventLoggerfromerrors/loggerlogger.emit()with aWideEventfor request-scoped logslogger.lifecycle()/logger.lifecycleWarning()for server startup/shutdown events
- Naming
camelCase— variables and functionsPascalCase— classes, types, enumsSCREAMING_SNAKE_CASE— module-level constants
- Database — use Drizzle ORM with transactions; validate all inputs before DB writes; handle unique constraint violations explicitly
- Dates — only use Luxon
DateTime; never use the built-inDate- Always work in UTC:
DateTime.utc(), neverDateTime.now()orDateTime.local() - Parse with
DateTime.fromISO(str, { zone: "utc" })— never omit{ zone: "utc" } - Call
.toUTC()on anyDateTimethat might have entered with a local zone
- Always work in UTC:
Generated types live in src/gen/ — don't edit them by hand. If you change a .proto definition, run bun run gen to regenerate.
Integration tests use Vitest and run against real infrastructure (Postgres and ClickHouse) on isolated ports. The test suite is run against both storage adapters to ensure adapter parity.
cp .env.example .env.testEdit .env.test to point at the test instances:
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/scrawn_test
CLICKHOUSE_URL=http://default:clickhouse@localhost:8124/scrawn_testdocker compose -f docker-compose.test.yml up -dThis starts PostgreSQL on port 5433 and ClickHouse on port 8124 — separate from the dev stack so both can run simultaneously.
# Both adapters (recommended before opening a PR)
bun run test:all
# Postgres only
bun run test:postgres
# ClickHouse only
bun run test:clickhouse
# Interactive UI
bun run test:uiThe test suite spins up gRPC (:18069) and Fastify (:18070) servers internally so tests don't conflict with a running dev server.
src/__tests__/
fixtures/
grpc.ts — gRPC client helpers and typed RPC wrappers
apiKey.ts — createTestApiKey() and other DB seed factories
db/
types.ts — NormalizedBasicUsageEvent, TestDBAdapter interface
index.ts — getTestDB() singleton (picks adapter from STORAGE_ADAPTER)
postgres.ts — PostgresTestDB — queries Drizzle, normalizes to shared shape
clickhouse.ts — ClickHouseTestDB — queries ClickHouse, normalizes to shared shape
assertions/
events.ts — verifyBasicUsageEventStored() and friends
setup.ts — Vitest globalSetup: starts servers, wires DB connections
events.test.ts — EventService integration tests
When adding new assertions, implement findX() on both PostgresTestDB and ClickHouseTestDB, then write the assertion function in assertions/ against the normalized shape — no if (STORAGE_ADAPTER) branching in assertions.
| Script | Description |
|---|---|
bun run dev:backend |
Start dev server with auto-reload |
bun start |
Start production server |
bun run test:all |
Run integration tests against both adapters |
bun run test:postgres |
Run integration tests against Postgres only |
bun run test:clickhouse |
Run integration tests against ClickHouse only |
bun run test:ui |
Open Vitest UI |
bun run typecheck |
Type-check with tsgo |
bun run format |
Format all files with Prettier |
bun run gen |
Regenerate protobuf types from proto/ |
bun run migrate:clickhouse |
Run ClickHouse schema migrations |
bun run proto:pull |
Pull latest proto submodule changes |
bun run init_key |
Generate an initial dashboard API key |
- Branch off
mainwith a descriptive name:feat/my-feature,fix/the-bug,refactor/thing - Follow the code style conventions above — Husky will run
formatandtypecheckon commit - Add or update tests for any behaviour change; make sure
bun run test:allpasses - Keep commits focused — one logical change per commit with a clear message
- Open the PR against
mainwith a description of what changed and why
For significant changes, open an issue first to discuss the approach before writing code.