FORGE 614
Launch your site
04.D
Developer Docs
Forge App Store

Build an app. Keep 70%.

The Forge platform is open to third-party developers. Build apps for auto repair shops, dealerships, fleet operators, and racing teams. We handle tenant isolation, auth, billing, and hosting. You focus on the feature.

Revenue split
70%
You keep
/
30%
Platform fee
No upfront costs. No listing fees. You set the price, we collect and remit monthly. Stripe-powered payouts.
04.D.1
Getting started

Four steps to publish.

01

Scaffold your app

Run `npm run forge:scaffold-app <slug>` to generate the manifest and directory structure. Every app lives at apps/<slug>/ and exports a manifest.ts.

02

Define your manifest

Export a manifest object from apps/<slug>/manifest.ts. Declare tables (Drizzle), procedures (tRPC), routes (Next.js App Router), migrations, and pricing.

03

Register your app

Import your manifest in src/lib/apps.ts. Add it to the `allManifests` array — the registry discovers it automatically at runtime.

04

Implement & deploy

Build your UI under src/app/admin/<slug>/, add Drizzle migrations to drizzle/, and deploy. The system handles tenant isolation, auth, and billing.

04.D.2
Manifest spec

AppManifest interface

Every app in the Forge ecosystem is defined by a single manifest.ts file. This file declares the app's identity, dependencies, data model, API surface, and pricing.

Manifests live at apps/<slug>/manifest.ts and are dynamically loaded by the app registry. The type definition lives at src/types/app.ts.

src/types/app.ts — AppManifest
interface AppTable {
  name: string;          // SQL table name in Postgres
  label: string;         // Human-readable label
}

interface AppProcedure {
  slug: string;          // Unique identifier e.g. "process-payment"
  name: string;          // Human-readable name
}

interface AppRoute {
  path: string;          // URL path relative to app mount point
  description: string;   // What the page/endpoint does
}

interface AppMigration {
  file: string;          // File name relative to drizzle/ directory
  description: string;   // What the migration does
}

interface AppPricing {
  monthlyCents: number;  // Monthly price in USD cents (0 = free)
  stripePriceId?: string; // Optional Stripe price ID
}

interface AppDependency {
  slug: string;
  version?: string;
}

interface AppManifest {
  slug: string;          // Unique kebab-case identifier
  name: string;          // Human-readable name
  version: string;       // SemVer string
  description?: string;  // ≤ 200 chars
  dependsOn?: AppDependency[];
  exposes?: {
    tables?: AppTable[];
    procedures?: AppProcedure[];
    routes?: AppRoute[];
    migrations?: AppMigration[];
  };
  pricing: AppPricing;   // Zero = free
}

Field reference

FieldTypeRequiredDescription
slugstringRequiredUnique kebab-case identifier (e.g. 'website', 'inventory'). Used as the app's primary key throughout the system.
namestringRequiredHuman-readable display name shown in the App Store.
versionstringRequiredSemVer version string (e.g. '0.1.0'). The app version is independent of the platform version.
descriptionstringOptionalShort description (≤ 200 chars). Appears in the App Store tile and API response.
dependsOnAppDependency[]OptionalOther apps this app depends on. Dependencies are auto-installed with the app.
exposes.tablesAppTable[]OptionalDrizzle tables the app registers. These are created during tenant installation.
exposes.proceduresAppProcedure[]OptionaltRPC procedures the app exposes. Accessible at /api/trpc.
exposes.routesAppRoute[]OptionalNext.js App Router routes relative to the app's mount point.
exposes.migrationsAppMigration[]OptionalDrizzle Kit migration file identifiers. Run during app install/upgrade.
pricing.monthlyCentsnumberRequiredMonthly price in USD cents. Set to 0 for free apps.
pricing.stripePriceIdstringOptionalStripe price ID for billable apps. Required if monthlyCents > 0.
04.D.3
Examples

Real manifests from the repo.

apps/website/manifest.ts

Website Builder

The Website Builder app demonstrates a complete manifest with two database tables, two tRPC procedures, two admin routes, a migration, and Stripe billing at $19.99/mo.

Key patterns: Declares both schema tables and admin UI routes. Uses stripePriceId for connected billing. No app dependencies.

apps/website/manifest.ts
import type { AppManifest } from "../../src/types/app";

const manifest: AppManifest = {
  slug: "website",
  name: "Website Builder",
  version: "0.1.0",
  description:
    "Build and publish your shop's public-facing website with drag-and-drop blocks.",
  dependsOn: [],
  exposes: {
    tables: [
      { name: "website_pages", label: "Pages" },
      { name: "website_blocks", label: "Blocks" },
    ],
    procedures: [
      { slug: "publish-site", name: "Publish Site" },
      { slug: "preview-site", name: "Preview Site" },
    ],
    routes: [
      { path: "/admin/website", description: "Website builder dashboard" },
      { path: "/admin/website/pages", description: "Page editor" },
    ],
    migrations: [
      { file: "0005_create_website_tables", description: "Create website tables" },
    ],
  },
  pricing: {
    monthlyCents: 1999,
    stripePriceId: "price_website_monthly",
  },
};

export default manifest;
apps/inventory/manifest.ts

Inventory Manager

A free, open-manifest app. Shows how to build a zero-cost utility app with categories, items, stock adjustments, and reorder alerts. No Stripe integration needed.

Key patterns: Free app with monthlyCents: 0. Declares three admin routes. Two tRPC procedures for stock operations. No Stripe price ID.

apps/inventory/manifest.ts
import type { AppManifest } from "../../src/types/app";

const manifest: AppManifest = {
  slug: "inventory",
  name: "Inventory Manager",
  version: "0.1.0",
  description: "Manage parts inventory, stock levels, and reorder alerts.",
  dependsOn: [],
  exposes: {
    tables: [
      { name: "inventory_items", label: "Inventory Items" },
      { name: "inventory_categories", label: "Categories" },
    ],
    procedures: [
      { slug: "adjust-stock", name: "Adjust Stock" },
      { slug: "reorder-report", name: "Reorder Report" },
    ],
    routes: [
      { path: "/admin/inventory", description: "Inventory dashboard" },
      { path: "/admin/inventory/items", description: "Item listing" },
      { path: "/admin/inventory/categories", description: "Categories" },
    ],
    migrations: [
      { file: "0004_create_inventory_tables", description: "Create inventory tables" },
    ],
  },
  pricing: {
    monthlyCents: 0,
  },
};

export default manifest;
04.D.4
tRPC API

API surface

All Forge APIs are exposed via tRPC at /api/trpc. The router is defined in src/lib/trpc/router.ts and uses Zod for input validation and SuperJSON for serialization.

App-specific procedures are registered via the manifest's exposes.procedures field. The platform router handles authentication, tenant resolution, and context injection automatically.

src/lib/trpc/router.ts
// src/lib/trpc/router.ts
import { z } from "zod";
import { initTRPC } from "@trpc/server";
import superjson from "superjson";

const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

export const appRouter = t.router({
  health: publicProcedure.query(() => {
    return { status: "ok", timestamp: new Date().toISOString() };
  }),

  listTenants: publicProcedure.query(async ({ ctx }) => {
    const rows = await ctx.db.select().from(tenants);
    return rows;
  }),

  getTenantById: publicProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ ctx, input }) => {
      const [row] = await ctx.db
        .select()
        .from(tenants)
        .where(eq(tenants.id, input.id))
        .limit(1);
      return row ?? null;
    }),
});

export type AppRouter = typeof appRouter;
04.D.5
App registry

How apps are discovered

The app registry at src/lib/apps.ts dynamically loads all manifests from apps/*/manifest.ts. When you add a new app, import its manifest in the registry and it becomes available across the platform.

Current catalog
Website Builder
website
$19.99/mo
Inventory Manager
inventory
Free
Apply now

Build for the bay floor.
Ship in weeks.

Columbus-based developers preferred. Remote OK for the right fit. 20 hrs/week contract, $60–$100/hr.

Apply as a developer →Or email us directly