Thanks for your interest in contributing! This guide covers everything you need to get started.
Bug fixes, tests, docs, and small self-contained improvements are welcome as direct pull requests — no need to ask first.
New features, behavior changes, new dependencies, new endpoints, schema/migration changes, or anything architectural must start as an issue so we can agree on the feature and the approach before any code is written. A PR for one of these without an agreed issue will likely be closed unmerged — not because the idea is bad, but because settling direction after the code exists wastes your effort and ours. The flow:
- Open an issue describing the problem, the change you propose, and how you'd approach it.
- Wait for a maintainer to agree on scope + approach (usually quick).
- Implement it, and link the issue in your PR.
If you're unsure whether something counts as a "fix" or a "feature," open an issue and ask — that's always the cheaper path.
Every PR is reviewed by a human, so make it easy to trust:
- One change per PR. One bug, or one agreed feature. Don't bundle unrelated changes.
- Scope the diff. Touch only the files your change needs. Do not reformat unrelated lines
or "fix" pre-existing Prettier/lint drift on lines you aren't otherwise changing — run
bun format, then review the diff and drop anything unrelated before you push. - Explain the why. State what was broken (or what the linked issue agreed), and exactly how you verified it — the commands you ran and the before/after behavior.
- Prove it. Add a test that fails without your change and passes with it, and say so in the PR.
- No test spam. A test earns its place by catching a regression that could actually happen. Don't add tests to move a coverage number, and don't submit ones that assert a constant equals itself, re-check what the type system already guarantees, only verify that a mock you just wrote was called, or restate the implementation line by line. Those pass forever, catch nothing, and every future contributor pays to read and maintain them. Coverage percentage is not a review criterion — one test that genuinely fails without your change is worth more than twenty that can't fail at all.
- Green before you open.
bun run test, the relevant typecheck (bun run --cwd <workspace> lint), andbun formatall pass locally.
AI tools are fine to use — but you are the author and are accountable for every line you submit:
- Understand your whole diff. If you can't explain a line in review, don't submit it.
- Verify, don't trust. Actually run the change and confirm it does what the PR claims. Do not paste generated code — or a generated PR description — that you haven't checked against the real codebase.
- Keep it real and scoped. The PRs that waste the most review time are plausible-looking but unverified: invented/nonexistent APIs, fixes for bugs that don't exist, sweeping reformats, or duplicates of open work. Low-effort, speculative, or spammy PRs (AI-generated or not) are closed on sight.
A focused, verified, well-explained PR — AI-assisted or not — is exactly what we want.
- Bun 1.3.10 (pinned in
.bun-versionandpackage.json) - Node.js 22 or newer (see
.nvmrcandpackage.json) - Docker, when using the Compose stack or testing Docker-based deployments
git clone https://github.com/oblien/openship.git
cd openship
bun install --frozen-lockfile
cp apps/api/.env.example apps/api/.env
cp apps/dashboard/.env.example apps/dashboard/.env
bun devThis starts the API and dashboard used for local development:
| Service | URL |
|---|---|
| Dashboard | http://localhost:3001 |
| API | http://localhost:4000 |
Use the root scripts to run a different workspace or the full development graph:
bun dev:api # API and its workspace dependencies
bun dev:dashboard # Dashboard only
bun dev:web # Marketing site (http://localhost:3009)
bun dev:desktop # Electron desktop app
bun dev:email # Email server and client
bun dev # All workspace dev tasksThe root .env.example is for the Docker Compose stack. To run that stack instead, copy it
to .env and run docker compose up -d --build. Compose starts PostgreSQL, Redis, the API,
the dashboard, and the web app; its web app is exposed at http://localhost:3000.
apps/
api/ → Hono API engine (port 4000)
cli/ → CLI tool (`openship deploy`)
dashboard/ → Next.js deployment dashboard (port 3001)
desktop/ → Electron desktop app and local service launcher
email/ → Email engine and Zero server/client orchestrator
web/ → Next.js marketing site (port 3009 in development)
packages/
adapters/ → Docker, bare, and cloud runtimes plus infrastructure adapters
core/ → Shared types, constants, utilities, errors
db/ → Drizzle ORM schema + client + repositories
db-email/ → Email-server Drizzle schemas + client
onboarding/ → Shared onboarding flows, validation, and API client
ui/ → Shared React components (Tailwind)
Most workspaces extend tsconfig.base.json at the repo root. The email client and server
maintain their own strict TypeScript configurations.
- Commits: Conventional Commits -
feat:,fix:,docs:,chore: - Branches:
feat/,fix/,docs/,chore/ - Code style: Prettier - run
bun formatbefore committing - Types: TypeScript strict mode everywhere
Dashboard dictionaries live in apps/dashboard/src/i18n/locales/<locale>/. When adding or
updating a locale:
- Keep the same files, nested keys, and interpolation placeholders (for example
{name}) as the English dictionaries inapps/dashboard/src/i18n/locales/en/. - Register the locale in
apps/dashboard/src/i18n/index.tsand expose it in the dashboard and onboarding language selectors. - Add a translated README under
docs/i18n/README.<locale>.md, then add its language badge to the root README and every localized README. - Preserve product names, commands, URLs, code blocks, and established technical terms.
- Run the dashboard tests, TypeScript check, and Prettier before submitting the change.
Each API module lives in apps/api/src/modules/<name>/ and follows this structure:
<name>.routes.ts # Route definitions (Hono)
<name>.controller.ts # Request handlers
<name>.service.ts # Business logic
<name>.schema.ts # TypeBox validation schemas
Shared modules (auth, projects, deployments, domains, webhooks, health) are always mounted. The billing module is cloud-only - it's only mounted when CLOUD_MODE=true in the environment.
If you're adding something that should only exist in the cloud version:
- Gate it behind
CLOUD_MODEinapps/api/src/app.ts - Make any required env vars (like Stripe keys) optional in
apps/api/src/config/env.ts - Self-hosters should never see 500s from missing cloud config
The one-click Apps catalog is data, not code — adding one is a small pull request that adds a single JSON file. No TypeScript required.
- Write
packages/core/src/apps/catalog/<id>.json(start it with"$schema": "https://openship.io/app.schema.json"for editor autocomplete) - Regenerate the merged artifact and validate:
cd packages/core
bun scripts/gen-catalog.ts # rewrites src/apps/catalog.json (a drift test fails CI without it)
bunx vitest run src/apps/catalog.test.ts # shape + referential validation for every app- Keep
"available": falseuntil it deploys cleanly end to end
Apps must be open-source, use an official image pinned to a version, and auto-generate any credentials. Full walkthrough and field reference:
- Add an app — builds a real two-service app step by step
- App catalog JSON — every field
bun db:generate # Generate Drizzle migration files from schema
bun db:push # Push schema to dev database (no migration file)
bun db:migrate # Run pending migrations (production)
bun run --cwd packages/db db:studio # Open Drizzle Studio (database browser)Schema lives in packages/db/src/schema/.
The root test and build scripts run the corresponding tasks across the workspaces that define them:
bun run test
bun run buildRun workspace checks directly when working on a single area. For example, the API typecheck
is bun run --cwd apps/api lint. Run bun format before committing and review the resulting
diff so unrelated files are not included.
Open an issue or start a discussion - happy to help!