Contributing
This guide covers the mechanics of making a change to Surplus — getting set up, meeting the quality bar, and getting your work validated. If you're brand new, start with the Quickstart.
Before you start
Make sure you can run the stack and that a clean checkout passes:
bun install
bun run typecheck # types across every package
bunx biome check . # read-only lint + format (CI uses this)
bun run knip:ci # unused files and dependenciesThese should pass on production / staging before you begin, so you know any failures afterward are yours.
Make your change
Surplus rewards working with its structure rather than around it:
- Follow the conventions. camelCase, no
any, semantic UI tokens, stable element IDs, transactions for multi-step writes. These aren't style preferences — they're what keeps the codebase navigable and the tests reliable. - Adding or changing an endpoint? Touch the four layers in order — contract, service function, service class, route handler — as described in Request lifecycle. The contract change is what keeps the client and server in sync.
- Changing the database? Edit the Drizzle schema in
packages/postgres/src/schema/(the source of truth) and apply it with the Drizzle push workflow. Don't hand-write ad-hoc SQL for routine schema changes. - Reuse types. Derive from
@surplus/typeswith.pick(),.omit(),.partial(), and.extend()instead of redefining fields.
Validate locally
Before opening a pull request, run the full set of checks:
bun run typecheck
bun run lint # Biome check + autofix (CI runs bunx biome check . read-only)
bun run knip:ci
bun run test # Playwright end-to-end suite (ephemeral infra)The end-to-end suite spins up a throwaway Neon branch and Upstash instance, so it's safe to run repeatedly. See Testing for how to add coverage for your change — new behavior should come with a test.
Opening a pull request
New pull requests are pre-filled from .github/pull_request_template.md. Fill in each section:
- Problem — what you're fixing or building. Link related issues with
Closes #123orPart of #123when applicable. - Solution — a plain-English summary of what you implemented. Reviewers can scan the diff for file-level detail.
- Screenshots / Recordings — optional; include for UI changes or delete the section if not applicable.
- Notes — follow-up work, known gaps, rollout or migration context, and how you validated locally (for example,
bun run lint,bun run typecheck,bun run test).
A few habits that help reviewers:
- Keep PRs small and focused — one logical change per pull request when you can.
- Call out schema changes, cache invalidation, or deploy-sensitive behavior in Notes so nothing surprises production.
- Use Screenshots / Recordings for client UI work — overlays and semantic tokens are easier to review with visuals.
- Don't discuss security vulnerabilities in a public PR; use Vulnerability disclosure instead.
Continuous integration
Pull requests targeting production or staging automatically run quality (Biome, typecheck, Knip) and E2E (Playwright) in GitHub Actions. E2E injects configuration through the Railway CLI against the same kind of ephemeral infrastructure you use locally. Get CI green before requesting a merge. See Testing for details.
Commit messages
Keep commits focused and write messages in the imperative, describing the change and the why — for example, "Add multi-distribution batch allocate for hub inventory" or "Fix logger not visible in Apitally." Group related work into a single logical commit rather than many noisy ones.
A quick checklist
-
bun run typecheckpasses -
bunx biome check .is clean (orbun run lintafter autofix) -
bun run knip:cipasses -
bun run testpasses, and new behavior is covered - The change follows the conventions
- New endpoints go through the contract and the four layers
- Schema changes are made in Drizzle, not ad-hoc SQL
Security issues
Found a vulnerability? Please don't open a public issue. See Vulnerability disclosure for how to report it responsibly.