Skip to main content
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.
FileWhat 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: "[email protected]",
    supportEmail: "[email protected]",
  },

  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:
cp .env.example .env

Required Variables

VariableDescriptionExample
API_URLPublic API URLhttps://api.yourdomain.com
FRONTEND_URLFrontend app URLhttps://app.yourdomain.com
DATABASE_URLTurso database URLlibsql://your-db.turso.io
DATABASE_AUTH_TOKENTurso auth tokeneyJ...
JWT_SECRETSecret for signing JWTsGenerate with openssl rand -hex 64
RESEND_API_KEYResend email API keyre_...
STRIPE_SECRET_KEYStripe secret keysk_live_...
STRIPE_WEBHOOK_SECRETStripe webhook signing secretwhsec_...

Optional Variables

VariableDescriptionDefault
PORTAPI server port3000
CORS_ORIGINSComma-separated allowed originshttp://localhost:5173,http://localhost:3000
REDIS_HOSTRedis host for queues and cachelocalhost
REDIS_PORTRedis port6379
S3_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:
1

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",
2

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"],
},
3

Enforce in routes

Use the requirePermission middleware:
import { requirePermission } from "../middleware/roles";

.get("/", handler, {
  beforeHandle: [organizationMiddleware, requirePermission("widgets:read")],
})

Next Steps