Skip to content

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.

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)
RepositoryPurposeDeploys to
honeycombSocial platform + thin extension UI shellsCloudflare Pages
honeycomb-apiExtension runtime + core servicesRailway / Fly.io
honeycomb-ext-{name}Individual extension packagenpm 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.

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)
  1. Clean boundaries — A booking table cannot accidentally break the posts table.
  2. Independent migrations — Update the booking schema without touching social tables.
  3. Permission scoping — RLS policies per schema; users only access extension data they have installed.
  4. Future-proof — Third-party extensions will each get their own schema. The pattern is already established.

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.id

The public schema is the spine. Extensions are ribs that attach to it.

Extensions are tracked in two core tables in the public schema:

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.

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.ts
const { 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.

When a user creates a Business Account, they select what kind of business they run. Each type activates a relevant set of extensions:

Business TypeExtensions 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 / TeamProject 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).

Extensions interact with the core platform through a set of injected services. They never access the public schema directly.

ServiceMethodsPurpose
dbStandard Supabase clientQueries the extension’s own schema
publicDbStandard Supabase clientRead-only access to public schema
paymentscreateCheckout, processRefundStripe Connect payments with platform fee
notificationssendIn-app notifications via the existing realtime system
feedpublishActivityCreates posts in the social feed
messagingsendSystemMessageSends messages in existing chats
storageupload, getPublicUrlFile 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.

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 LevelWho Can CallExamples
publicAnyone, even unauthenticatedView services, browse portfolio
authenticatedAny logged-in userBook an appointment, buy a product
ownerThe user who installed the extensionManage services, view their appointments
adminPlatform admin or system webhooksPayment callbacks, moderation

The runtime enforces access levels automatically. If onAction is called, the caller already has permission.

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 it

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 status
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.json

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" },
// ...
],
};

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}`);
}
},
};

Create the extension’s Postgres schema, tables, RLS policies, and grants:

CREATE SCHEMA IF NOT EXISTS ext_<name>;
-- Tables...
-- Required grants for Supabase RLS
GRANT USAGE ON SCHEMA ext_<name> TO authenticated, anon;
GRANT ALL ON ALL TABLES IN SCHEMA ext_<name> TO authenticated;
-- RLS policies on every table
ALTER TABLE ext_<name>.<table> ENABLE ROW LEVEL SECURITY;
-- ...
  1. Publish the package to the npm registry.
  2. Install it in honeycomb-api and register the handler in the extension registry.
  3. Apply the SQL migrations against the Supabase database.
  4. Seed a row in the apps table so users can discover and install it.
  5. Build UI in honeycomb under src/lib/modules/<name>/ with thin Svelte components that call callExtension('<name>', '<action>', data).

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.ts

Extension 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.

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.