Skip to main content

Overview

BunShip is a monorepo built with Turborepo that separates concerns into apps (deployable services) and packages (shared libraries). Every package is written in TypeScript, and the entire stack runs on Bun for both development and production.

Monorepo Structure

Package Descriptions

Package Dependency Graph

The dependency flow is intentionally one-directional. Packages at the bottom of the graph never import from packages above them.
@bunship/eden depends on the API’s types, not its runtime code. This means your frontend gets full autocomplete without bundling the server.

Request Lifecycle

Every HTTP request to the API passes through a predictable middleware chain before reaching the route handler.
1

Elysia receives the request

Bun’s HTTP server hands the request to Elysia, which parses the URL, method, headers, and body.
2

Global middleware runs

CORS, rate limiting, request logging, and body size validation are applied to all routes.
3

Auth middleware resolves the user

The authMiddleware extracts the Bearer token from the Authorization header, verifies it with jose, and loads the user from the database.
4

Organization middleware resolves the tenant

For organization-scoped routes (/api/v1/organizations/:orgId/*), the organizationMiddleware loads the organization and the user’s membership in a single pass.
5

Permission middleware checks RBAC

requirePermission() or requireRole() verifies the user’s role grants the specific permission needed for this operation.
6

Route handler executes

The handler calls into a service function that contains the business logic. Services interact with the database through Drizzle and return plain objects.
7

Response is serialized

Elysia validates the response against the route’s TypeBox schema, serializes it to JSON, and sends it back to the client.

Key Design Decisions

BunShip targets Bun exclusively rather than maintaining Node.js compatibility. This unlocks Bun’s native crypto.subtle API, the built-in SQLite driver, faster startup times, and a single tool for runtime, package management, and test execution. The trade-off is that Bun must be available in your deployment environment.
Instead of PostgreSQL, BunShip uses SQLite locally and Turso (a libSQL-based distributed SQLite service) in production. Benefits:
  • Zero infrastructure for local development — the database is a file
  • Edge replication through Turso for global low-latency reads
  • Simpler operational model compared to managed PostgreSQL
  • Full SQL support through Drizzle ORM
The database schema uses integer timestamps and text-based IDs (CUID2) to stay compatible with SQLite’s type system.
The API defines request/response schemas using Elysia’s TypeBox integration. These types flow through to:
  1. Route validation — Elysia rejects invalid payloads at the boundary
  2. Service layer — TypeScript enforces correct data shapes
  3. Database — Drizzle infers column types from the schema
  4. Client — Eden Treaty derives client types from the server’s route tree
No code generation step is needed. Types update automatically when you change a route definition.
Elysia plugins compose using .use(). BunShip chains middleware as independent Elysia instances:
Each middleware reads from and writes to the shared context (store), keeping individual pieces testable and replaceable.
All feature flags, billing plans, role permissions, and app settings live in @bunship/config as typed TypeScript objects. This means:
  • IDE autocomplete for every config value
  • Compile-time errors when you reference a config key that does not exist
  • No YAML/JSON parsing at runtime
  • A single import (@bunship/config) for any package that needs configuration

Application Configuration

The @bunship/config package exports four configuration modules:
Core application settings including the API prefix, JWT expiry times, CORS origins, and rate limits.

Database Layer

BunShip uses Drizzle ORM with SQLite. The schema is defined in packages/database/src/schema/ with one file per table: All IDs use CUID2 for collision-resistant, URL-safe identifiers. Timestamps are stored as SQLite integers (Unix epoch).