Extension System
Honeycomb’s extension system adds business tools on top of the core social platform. Extensions are first-party tools organized by business type — booking, e-commerce, invoicing, portfolio, CRM, and more. The architecture is designed so that third-party developers can build extensions using the same patterns in the future.
Architecture overview
Section titled “Architecture overview”The extension system spans three repositories:
┌─────────────────────┐ ┌──────────────────────┐│ honeycomb │ HTTP │ honeycomb-api ││ (SvelteKit) │ ──────> │ (Hono server) ││ Thin extension UI │ │ Extension runtime │└─────────────────────┘ │ Core services │ │ ┌────────────────┐ │ │ │ Loads installed │ │ │ │ extensions as │ │ │ │ npm packages │ │ │ └───────┬────────┘ │ └───────────┼──────────┘ ┌─────────┼─────────┐ v v v @mindhyv/ @mindhyv/ @mindhyv/ ext-booking ext-store ext-invoicing (npm pkg) (npm pkg) (npm pkg)| Repository | Purpose | Deploys to |
|---|---|---|
honeycomb | Social platform + thin extension UI shells | Cloudflare Pages |
honeycomb-api | Extension runtime + core services | Railway / Fly.io |
honeycomb-ext-{name} | Individual extension package | npm registry |
Each extension is an npm package that the API server loads at startup. The SvelteKit frontend renders thin UI components that call the API. No extension business logic lives in the frontend.
Database: schema separation
Section titled “Database: schema separation”The core social platform lives in the public Postgres schema (45+ tables). Each extension gets its own isolated schema:
public -> Social platform (profiles, posts, messages, wallet, etc.)ext_booking -> Booking system (services, availability, appointments)ext_store -> E-commerce (cart, orders, order items, coupons)ext_invoicing -> Invoicing (invoices, line items, reminders)ext_portfolio -> Portfolio (projects, galleries)ext_crm -> Client management (contacts, contact notes)Why separate schemas?
Section titled “Why separate schemas?”- Clean boundaries — A booking table cannot accidentally break the posts table.
- Independent migrations — Update the booking schema without touching social tables.
- Permission scoping — RLS policies per schema; users only access extension data they have installed.
- Future-proof — Third-party extensions will each get their own schema. The pattern is already established.
How extension schemas connect to public
Section titled “How extension schemas connect to public”Every extension table references public.profiles.id as the owner. Every transaction flows through public.payments. Content can be surfaced in the public.posts feed.
ext_booking.appointments -> provider_id references public.profiles.id (the business owner) -> client_id references public.profiles.id (the person booking) -> payment_id references public.payments.id (goes through the wallet)
ext_store.orders -> store_id references public.stores.id (already exists) -> buyer_id references public.profiles.id -> payment_id references public.payments.idThe public schema is the spine. Extensions are ribs that attach to it.
Extension registration
Section titled “Extension registration”Extensions are tracked in two core tables in the public schema:
apps (registry)
Section titled “apps (registry)”Stores the catalog of available extensions with their slug, name, icon, category, associated Postgres schema name, and configuration. Each row represents one extension that users can install.
user_apps (installations)
Section titled “user_apps (installations)”A join table linking users to the extensions they have installed. Tracks whether each installation is active. The (app)/+layout.server.ts loader fetches the current user’s active user_apps rows (joined with apps for slug, name, and icon) so the sidebar can display installed extension links.
// From (app)/+layout.server.tsconst { data: installedApps } = await locals.supabase .from('user_apps') .select('app_id, is_active, apps(slug, name, icon)') .eq('user_id', user!.id) .eq('is_active', true)When a user creates a Business Account and selects their business type, the relevant extensions are activated automatically.
Business types and tool activation
Section titled “Business types and tool activation”When a user creates a Business Account, they select what kind of business they run. Each type activates a relevant set of extensions:
| Business Type | Extensions Activated |
|---|---|
| Service Provider (freelancer, consultant, coach) | Booking, Invoicing, CRM |
| Seller (handmade, dropship, digital) | Store/Orders, CRM |
| Creator (writer, designer, photographer) | Portfolio, Digital Downloads, Tipping |
| Local Business (restaurant, salon, gym) | Booking, Menu/Catalog, Reviews |
| Agency / Team | Project Boards, Invoicing, CRM |
Users can run multiple business types simultaneously. A photographer might have a Portfolio (Creator), a Booking Calendar (Service Provider), and a Store for prints (Seller).
Services layer
Section titled “Services layer”Extensions interact with the core platform through a set of injected services. They never access the public schema directly.
| Service | Methods | Purpose |
|---|---|---|
db | Standard Supabase client | Queries the extension’s own schema |
publicDb | Standard Supabase client | Read-only access to public schema |
payments | createCheckout, processRefund | Stripe Connect payments with platform fee |
notifications | send | In-app notifications via the existing realtime system |
feed | publishActivity | Creates posts in the social feed |
messaging | sendSystemMessage | Sends messages in existing chats |
storage | upload, getPublicUrl | File storage via Supabase buckets |
These services are currently internal functions in src/lib/services/. When the platform opens to third-party developers, these same services become the Extension SDK API.
The extension interface
Section titled “The extension interface”Every extension implements the ExtensionHandler contract:
interface ExtensionManifest { slug: string; // 'booking' name: string; // 'Booking Calendar' version: string; // '1.0.0' description: string; schema: string; // 'ext_booking' requires: string[]; // ['payments', 'notifications'] actions: ActionDefinition[];}
interface ExtensionHandler { manifest: ExtensionManifest; onInstall?(ctx: ExtensionContext): Promise<void>; onUninstall?(ctx: ExtensionContext): Promise<void>; onAction(ctx: ExtensionContext): Promise<unknown>;}Actions define what the extension can do and who can call each action:
| Access Level | Who Can Call | Examples |
|---|---|---|
public | Anyone, even unauthenticated | View services, browse portfolio |
authenticated | Any logged-in user | Book an appointment, buy a product |
owner | The user who installed the extension | Manage services, view their appointments |
admin | Platform admin or system webhooks | Payment callbacks, moderation |
The runtime enforces access levels automatically. If onAction is called, the caller already has permission.
Request flow
Section titled “Request flow”1. SvelteKit -> POST honeycomb-api/ext Body: { extension: "booking", action: "book", data: {...}, owner_id: "..." }
2. Runtime checks: - Does "booking" exist in the apps table? - Does the owner have it installed in user_apps? - Does "book" exist in the manifest? - Does the caller match the action's access level?
3. Runtime builds ExtensionContext: - Injects core services (scoped db, payments, notifications, etc.) - Sets user info, extensionOwner, action, data
4. Runtime calls: handler.onAction(ctx)
5. Handler runs business logic using ctx.services
6. Handler returns result -> runtime sends response -> SvelteKit renders itPayment flow
Section titled “Payment flow”All money flows through Stripe Connect with an automatic platform fee:
Extension calls ctx.services.payments.createCheckout(...) |Core service creates Stripe Checkout Session -> application_fee_amount = platform cut -> transfer_data.destination = seller's connected account -> metadata includes extension_slug, reference_id, reference_type |Customer pays on Stripe Checkout |Stripe fires webhook -> honeycomb webhook handler -> Sees metadata.extension_slug = "booking" -> POSTs to honeycomb-api: /webhook/payment-succeeded |API server loads booking handler -> Calls handler.onAction({ action: "payment-succeeded", data: {...} }) |Handler confirms the booking, sends notification, updates statusBuilding a new extension
Section titled “Building a new extension”1. Scaffold the package
Section titled “1. Scaffold the package”honeycomb-ext-<name>/├── src/│ ├── manifest.ts # Declares actions, schema, permissions│ ├── handler.ts # Implements onAction with business logic│ └── index.ts # Exports { manifest, handler }├── migrations/│ └── 001_create_schema.sql # CREATE SCHEMA + tables + RLS + grants├── package.json # name: @mindhyv/ext-<name>└── tsconfig.json2. Define the manifest
Section titled “2. Define the manifest”Declare the extension’s slug, schema name, required services, and all actions with their access levels.
import type { ExtensionManifest } from "@mindhyv/extension-interface";
export const manifest: ExtensionManifest = { slug: "booking", name: "Booking Calendar", version: "1.0.0", description: "Accept appointments and manage your schedule", schema: "ext_booking", requires: ["payments", "notifications"], actions: [ { name: "create-service", access: "owner", description: "Create a bookable service" }, { name: "list-services", access: "public", description: "List services for a user" }, { name: "book", access: "authenticated", description: "Book an appointment" }, { name: "payment-succeeded", access: "admin", description: "Handle successful payment" }, // ... ],};3. Implement the handler
Section titled “3. Implement the handler”Route each action to its implementation. Use ctx.services for all cross-cutting concerns.
import type { ExtensionHandler, ExtensionContext } from "@mindhyv/extension-interface";import { manifest } from "./manifest";
export const handler: ExtensionHandler = { manifest,
async onInstall(ctx) { // Seed default data (e.g., Mon-Fri 9-5 availability) },
async onAction(ctx) { switch (ctx.action) { case "book": return bookAppointment(ctx); case "list-services": return listServices(ctx); // ... default: throw new Error(`Unknown action: ${ctx.action}`); } },};4. Write migrations
Section titled “4. Write migrations”Create the extension’s Postgres schema, tables, RLS policies, and grants:
CREATE SCHEMA IF NOT EXISTS ext_<name>;
-- Tables...
-- Required grants for Supabase RLSGRANT USAGE ON SCHEMA ext_<name> TO authenticated, anon;GRANT ALL ON ALL TABLES IN SCHEMA ext_<name> TO authenticated;
-- RLS policies on every tableALTER TABLE ext_<name>.<table> ENABLE ROW LEVEL SECURITY;-- ...5. Publish and register
Section titled “5. Publish and register”- Publish the package to the npm registry.
- Install it in
honeycomb-apiand register the handler in the extension registry. - Apply the SQL migrations against the Supabase database.
- Seed a row in the
appstable so users can discover and install it. - Build UI in
honeycombundersrc/lib/modules/<name>/with thin Svelte components that callcallExtension('<name>', '<action>', data).
6. Frontend module
Section titled “6. Frontend module”The SvelteKit side is a thin UI shell following the standard module structure:
src/lib/modules/<name>/├── index.ts├── schemas/│ └── ... # Zod schemas for forms├── components/│ └── ... # Svelte components for the extension UI└── types/ └── index.tsExtension routes live under src/routes/(app)/business/<name>/ for the owner dashboard and optionally under src/routes/(app)/profile/[username]/<name>/ for the public-facing view.
Social integration
Section titled “Social integration”Every extension connects back to the social graph:
- Products/services listed can appear in the feed, be shared, and be bookmarked.
- Bookings and orders create client relationships visible in the social graph.
- Completed transactions trigger review prompts that show on profiles.
- Payments flow through the existing wallet system.
- Activity generates notifications using the existing realtime system.
The social network is not separate from the business tools. The social network is the distribution channel for the business tools.