A universal, composable standard for writing backend functions that run anywhere.
Backend developers rewrite the same login flow, file upload, Stripe webhook, and notification system for every project. Every time — with small but painful platform-specific differences.
Existing solutions (npm packages, GitHub snippets) share code but not structure. You still have to adapt them to your project's routing, database, and runtime manually.
Open Function Spec (OFS) fixes this by defining a module structure that is agnostic of the runtime, framework, and database client — and ships with a CLI to install pre-built functions in one command.
Every OFS-compliant module contains exactly three files:
src/modules/<module-name>/
schema.ts — Database tables (Drizzle ORM)
core.ts — Business logic (framework-free)
adapter.ts — Framework connector (Hono, Express, etc.)
Defines the database tables using Drizzle ORM. No logic, no imports from other layers.
// src/modules/notes/schema.ts
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
export const notes = sqliteTable('notes', {
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
title: text('title').notNull(),
content: text('content').notNull().default(''),
slug: text('slug').notNull(),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
});
export type Note = typeof notes.$inferSelect;Contains ALL business rules. No HTTP framework. No Request/Response. No process.env.
// src/modules/notes/core.ts
import type { DrizzleDB } from '../../db/client';
import { notes } from './schema';
import { eq, desc } from 'drizzle-orm';
interface CoreContext {
db: DrizzleDB;
env?: Record<string, string>;
}
export async function listNotes(ctx: CoreContext, userId: string) {
return ctx.db
.select()
.from(notes)
.where(eq(notes.userId, userId))
.orderBy(desc(notes.createdAt));
}
export async function createNote(ctx: CoreContext, input: { userId: string; title: string; content?: string }) {
const id = crypto.randomUUID();
const now = Date.now();
const [note] = await ctx.db
.insert(notes)
.values({ id, userId: input.userId, title: input.title, content: input.content ?? '', slug: `note-${id.slice(0,8)}`, createdAt: now, updatedAt: now })
.returning();
return note;
}The Cardinal Rules of core.ts:
| ✅ Allowed | ❌ Forbidden |
|---|---|
import from 'drizzle-orm' |
import { Hono } from 'hono' |
throw new Error(...) |
return c.json({ error: '...' }, 400) |
return rawDataObject |
return new Response(...) |
ctx.env.MY_API_KEY |
process.env.MY_API_KEY |
Why this matters: A
core.tsfollowing these rules can be unit tested with standard Vitest in Node.js even if production runs on Cloudflare Workers.
Connects core.ts to a specific web framework. Only handles request parsing and response formatting.
// src/modules/notes/adapter.ts — Hono (Cloudflare Workers)
import { Hono } from 'hono';
import { createDb } from '../../db/client';
import { listNotes, createNote } from './core';
const notesRoute = new Hono<{ Bindings: { DB: D1Database } }>();
notesRoute.get('/', async (c) => {
const userId = c.req.query('userId');
if (!userId) return c.json({ error: 'userId required' }, 400);
const db = createDb(c.env.DB);
return c.json({ notes: await listNotes({ db }, userId) });
});
notesRoute.post('/', async (c) => {
const body = await c.req.json<{ userId: string; title: string }>();
const db = createDb(c.env.DB);
return c.json({ note: await createNote({ db }, body) }, 201);
});
export { notesRoute };Swapping from Hono to Express only requires changing this file.
core.tshas zero changes.
OFS is supported by the Aerostack CLI. Functions that follow this spec can be installed, tested, and published through it.
npx aerostack@latest init my-api
cd my-api
npx drizzle-kit push # Create database tables from schemas
npx wrangler dev # Start local Cloudflare Worker dev servernpx aerostack add stripe-checkoutWhat happens automatically:
- Downloads
src/modules/stripe-checkout/(schema, core, adapter) - Patches
src/index.tsto mount the route:app.route('/checkout', stripeCheckoutRoute) - Patches
src/db/schema.tsto export the new tables - Runs
npm install stripeif needed
npx aerostack publishmy-api/
├── src/
│ ├── index.ts # Entry: mounts all module routes
│ ├── db/
│ │ ├── client.ts # Drizzle factory: createDb(d1)
│ │ └── schema.ts # Re-exports all module schemas
│ ├── modules/
│ │ ├── _core/ # Base tables: users, sessions
│ │ │ └── schema.ts
│ │ ├── notes/ # Feature module (you built this)
│ │ │ ├── schema.ts
│ │ │ ├── core.ts
│ │ │ └── adapter.ts
│ │ └── stripe-checkout/ # Installed via `aerostack add`
│ │ ├── schema.ts
│ │ ├── core.ts
│ │ └── adapter.ts
│ └── lib/
│ └── string/
│ └── slugify.ts # Utility: no db, no http
├── drizzle.config.ts # Glob-discovers all module schemas
├── wrangler.toml
└── package.json
| Type | Location | Description |
|---|---|---|
| Feature | src/modules/<name>/ |
Has schema + core + adapter. Adds a new API route. |
| Utility | src/lib/<name>/ |
Stateless helper function. No database, no HTTP. |
An OFS-compliant core.ts is runtime-agnostic:
| Runtime | schema.ts |
core.ts |
adapter.ts |
|---|---|---|---|
| Cloudflare Workers | ✅ D1/sqlite | ✅ | ✅ Hono |
| Node.js 18+ | ✅ libsql/pg | ✅ | ✅ Express |
| Bun | ✅ libsql | ✅ | ✅ Hono/Elysia |
| Deno | ✅ |
All OFS-compliant functions are listed at hub.aerostack.dev
The registry is searchable by category, tags, and runtime. Each function page shows its source, documentation, and a one-line install command.
We welcome contributions! See CONTRIBUTING.md for the full guide.
Quick checklist for a new function:
-
schema.tsuses Drizzle ORM (no raw SQL strings) -
core.tshas no framework imports (hono,express, etc.) -
core.tshas noprocess.env— usesctx.envonly -
adapter.tshas no business logic -
README.mddocuments endpoints, env vars, and tables - Passes
npx tsc --noEmit
Read the formal specification: SPEC.md
MIT. Build anything.
Built by the Aerostack team. The Aerostack Registry is the reference implementation of this standard.