BunShip uses a configuration-driven architecture. Instead of hunting through source code, you control most behavior by editing four files in the packages/config/src/ directory.
Config Package Overview
The @bunship/config package exports all shared configuration used across the monorepo.
File What it controls app.tsApplication name, URLs, API server settings, JWT, CORS features.tsFeature toggles, auth behavior, org settings, webhooks, jobs billing.tsStripe plans, pricing tiers, usage limits permissions.tsPermission definitions, role-based access control
Import any config value from the package:
import { appConfig , featuresConfig , billingConfig } from "@bunship/config" ;
Application Config
The appConfig object in packages/config/src/app.ts defines your application identity and server behavior.
export const appConfig = {
name: "YourSaaS" ,
description: "Your awesome SaaS product" ,
url: process . env . API_URL ?? "http://localhost:3000" ,
frontendUrl: process . env . FRONTEND_URL ?? "http://localhost:5173" ,
api: {
prefix: "/api/v1" ,
port: parseInt ( process . env . PORT ?? "3000" , 10 ),
host: "0.0.0.0" ,
rateLimit: {
enabled: true ,
windowMs: 60 * 1000 ,
maxRequests: 100 ,
},
cors: {
enabled: true ,
origins: [ "http://localhost:5173" , "https://yourdomain.com" ],
credentials: true ,
},
maxBodySize: "10mb" ,
timeout: 30000 ,
},
jwt: {
accessTokenExpiry: "15m" ,
refreshTokenExpiry: "7d" ,
issuer: "yoursaas" ,
},
company: {
name: "Your Company Inc." ,
email: "hello@yourdomain.com" ,
supportEmail: "support@yourdomain.com" ,
},
docs: {
enabled: true ,
path: "/docs" ,
title: "YourSaaS API" ,
description: "API documentation for YourSaaS" ,
version: "1.0.0" ,
},
} as const ;
Start by changing name, description, and company to match your product. These values
propagate to email templates, API docs, and error messages.
Environment Variables
All environment-specific values are read from .env. Copy .env.example to get started:
Required Variables
Variable Description Example API_URLPublic API URL https://api.yourdomain.comFRONTEND_URLFrontend app URL https://app.yourdomain.comDATABASE_URLTurso database URL libsql://your-db.turso.ioDATABASE_AUTH_TOKENTurso auth token eyJ...JWT_SECRETAccess token signing secret Generate one JWT_REFRESH_SECRETRefresh token signing secret Generate one RESEND_API_KEYResend email API key re_...STRIPE_SECRET_KEYStripe secret key sk_live_...STRIPE_WEBHOOK_SECRETStripe webhook signing secret whsec_...
Optional Variables
Variable Description Default PORTAPI server port 3000CORS_ORIGINSComma-separated allowed origins http://localhost:5173,http://localhost:3000REDIS_HOSTRedis host for queues and cache localhostREDIS_PORTRedis port 6379S3_BUCKETS3 bucket for file uploads - S3_REGIONS3 region - S3_ACCESS_KEY_IDS3 access key - S3_SECRET_ACCESS_KEYS3 secret key -
Stripe Price IDs
Each billing plan needs corresponding Stripe price IDs:
STRIPE_PRO_MONTHLY_PRICE_ID = price_1Abc...
STRIPE_PRO_YEARLY_PRICE_ID = price_1Xyz...
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID = price_1Def...
STRIPE_ENTERPRISE_YEARLY_PRICE_ID = price_1Ghi...
Feature Flags
The featuresConfig object in packages/config/src/features.ts controls which features are active and how they behave.
Authentication
auth : {
enableEmailPassword : true ,
enableMagicLink : true ,
enableGoogleOAuth : true ,
enableGithubOAuth : true ,
enableTwoFactor : true ,
enableSessionManagement : true ,
requireEmailVerification : true ,
password : {
minLength : 8 ,
requireUppercase : true ,
requireLowercase : true ,
requireNumber : true ,
requireSpecialChar : false ,
},
lockout : {
enabled : true ,
maxAttempts : 5 ,
lockoutDuration : 15 * 60 , // 15 minutes in seconds
},
maxSessionsPerUser : 5 ,
},
Organizations
organizations : {
enabled : true ,
allowMultipleOrgs : true ,
allowOrgCreation : true ,
requireOrgOnSignup : false ,
maxOrgsPerUser : 10 ,
roles : [ "owner" , "admin" , "member" , "viewer" ] as const ,
defaultRole : "member" as const ,
},
Disabling Features
Set enabled: false on any feature block to turn it off entirely:
// Turn off file uploads
fileUploads : {
enabled : false ,
},
// Turn off webhooks
webhooks : {
enabled : false ,
},
// Turn off audit logging
auditLogs : {
enabled : false ,
},
Disabling features removes their routes from the API. Existing data in the database is not
affected, but the endpoints will return 404.
CORS Settings
CORS origins are configured in two places. The config file sets defaults, while the CORS_ORIGINS environment variable overrides them at runtime.
// packages/config/src/app.ts
api : {
cors : {
enabled : true ,
origins : ( process . env . CORS_ORIGINS ?? "http://localhost:5173,http://localhost:3000" ). split ( "," ),
credentials : true ,
},
},
For production, set the environment variable with your actual domains:
CORS_ORIGINS = https://app.yourdomain.com,https://admin.yourdomain.com
To disable CORS entirely (not recommended for browser-facing APIs):
cors : {
enabled : false ,
},
Rate Limiting
BunShip applies rate limiting at two levels.
Global Rate Limit
Defined in appConfig.api.rateLimit, this applies to all routes:
rateLimit : {
enabled : true ,
windowMs : 60 * 1000 , // 1 minute window
maxRequests : 100 , // 100 requests per window per IP
},
Route-Level Rate Limit
Sensitive routes like authentication have tighter limits applied directly in the route definition using the elysia-rate-limit plugin:
import { rateLimit } from "elysia-rate-limit" ;
export const authRoutes = new Elysia ({ prefix: "/auth" }). use (
rateLimit ({
max: 20 ,
duration: 60 * 1000 ,
scoping: "scoped" ,
generator : ( req , server ) => server ?. requestIP ( req )?. address ?? "unknown" ,
})
);
API Key Rate Limits
API keys have their own rate limit defined in the features config:
apiKeys : {
enabled : true ,
maxKeysPerOrg : 10 ,
defaultRateLimit : 1000 , // requests per minute
},
Billing Configuration
Edit packages/config/src/billing.ts to define your pricing tiers. Each plan specifies a price, Stripe price IDs, usage limits, and feature descriptions.
export const billingConfig = {
currency: "usd" ,
plans: [
{
id: "free" ,
name: "Free" ,
description: "For side projects and experimentation" ,
price: { monthly: 0 , yearly: 0 },
stripePriceIds: { monthly: null , yearly: null },
limits: {
members: 2 ,
projects: 3 ,
apiRequests: 1000 ,
webhookEndpoints: 1 ,
apiKeys: 1 ,
storageGB: 0.5 ,
},
features: [
"Up to 2 team members" ,
"3 projects" ,
"1,000 API requests/month" ,
"Community support" ,
],
},
// Add more plans...
],
};
After modifying plans in code, you must create matching products and prices in your Stripe
Dashboard and update the stripePriceIds with the
generated price IDs.
Helper Functions
The billing config exports utility functions for checking limits:
import { getPlan , isWithinLimit , isUnlimited } from "@bunship/config" ;
const plan = getPlan ( "pro" );
if ( plan && isWithinLimit ( currentUsage , plan . limits . apiRequests )) {
// Allow the request
}
Permissions
The permission system is defined in packages/config/src/permissions.ts. Permissions follow a resource:action pattern with wildcard support.
export const permissions = {
"org:read" : "View organization details" ,
"org:update" : "Update organization settings" ,
"org:delete" : "Delete organization" ,
"members:read" : "View team members" ,
"members:invite" : "Invite new members" ,
"members:*" : "Full member management" ,
"projects:*" : "Full project management" ,
"billing:*" : "Full billing management" ,
// ...
} as const ;
Assigning Permissions to Roles
Role-permission mappings live in featuresConfig.organizations.permissions:
permissions : {
owner : [ "*" ], // Full access to everything
admin : [ "org:read" , "org:update" , "members:*" , "projects:*" ],
member : [ "org:read" , "members:read" , "projects:*" ],
viewer : [ "org:read" , "members:read" , "projects:read" ],
},
Wildcard Rules
"*" grants all permissions (used for the owner role)
"resource:*" grants all actions on a resource (e.g., "members:*" grants members:read, members:invite, members:update, members:remove)
Adding Custom Permissions
To add permissions for a new resource:
Define the permissions
Add entries to packages/config/src/permissions.ts: "widgets:read" : "View widgets" ,
"widgets:create" : "Create widgets" ,
"widgets:update" : "Update widgets" ,
"widgets:delete" : "Delete widgets" ,
"widgets:*" : "Full widget management" ,
Assign to roles
Update the role mappings in packages/config/src/features.ts: permissions : {
owner : [ "*" ],
admin : [ ... existingPerms , "widgets:*" ],
member : [ ... existingPerms , "widgets:read" , "widgets:create" ],
viewer : [ ... existingPerms , "widgets:read" ],
},
Enforce in routes
Use the requirePermission middleware: import { requirePermission } from "../middleware/roles" ;
. get ( "/" , handler , {
beforeHandle: [ organizationMiddleware , requirePermission ( "widgets:read" )],
})
Next Steps
Adding Routes Create new API endpoints with Elysia
Database Schema Modify and extend the database with Drizzle ORM