Secure, typesafe Hono APIs. Compiled.
Define your data model.
Get a multi-tenant, role-based, audit-logged API in seconds — and your week back.
Now go build the features that actually matter.
Built for the 2026 stack
Not a platform. A build-step.
Quickback compiles your schema and security rules into real Hono code on your infrastructure. Nothing runs on our servers. If you uninstall us tomorrow, your app still ships.
Policy in. Execution out.
Declarative policy becomes executable code.
Every Quickback rule — security, access, masking, workflow — is a TypeScript declaration that the compiler turns into the runtime code that enforces it. Not a runtime engine reading a config file. The enforcement is the build output.
Declarative Policy
Schema, firewall, access, guards, masking, actions — one TypeScript file per resource.
Compiler
Validates rules, resolves precedence, refuses to emit code that can be called without its declared security.
Enforced Execution
Hono routes, Drizzle constraints, Better Auth wiring, MCP tool guards — your policy, expressed as code.
No runtime policy engine to drift from your code. No separate service to keep in sync. The compiler is the policy enforcer — at build time, not request time.
Security, compiled in.
Four layers between your API and a breach. All declared. All compiled.
Firewall
Every table needs a scope column. Tenant isolation enforced at the database level.
Access
Role-based permissions. Deny by default. Compiled into middleware.
Guards
Field-level protection. Only permitted fields can be written.
Masking
PII redaction in responses. Sensitive data never leaks.
Plus a fifth pillar
Workflows
Actions + protected fields + record-level conditions, compiled into state transitions you can trust. Define approveInvoice or advance once — the compiler enforces who can transition, when, and which fields can change at each step.
Schema in. Hono out.
We never connect to your database. We never see your data. We operate at the schema level.
From the same definitions, the compiler emits a typed Hono API, an MCP server for LLMs, Drizzle migrations, and Better Auth wiring — standard TypeScript you own.
SEE IT IN ACTION
Define once. Compile a secure Hono API — or RLS policies for existing Supabase.
import { feature, q } from "@quickback/compiler";
export default feature("candidates", {
columns: {
id: q.id(),
name: q.text().required().filterable("like").searchable(),
email: q.text().required().filterable("eq").searchable(),
phone: q.text().optional().filterable("eq"),
resumeUrl: q.text().optional(),
source: q.enum(["linkedin", "referral", "careers-page", "other"]).default("other").required().filterable("eq"),
internalNotes: q.text().optional().searchable(),
organizationId: q.scope("organization"),
},
// firewall omitted — q.scope("organization") auto-derives
// [{ field: "organizationId", equals: "ctx.activeOrgId" },
// { field: "deletedAt", isNull: true }]
guards: {
createable: ["name", "email", "phone", "resumeUrl", "source", "internalNotes"],
updatable: ["name", "email", "phone", "resumeUrl", "source", "internalNotes"],
},
masking: {
email: { type: "email", show: { roles: ["admin+"] } },
phone: { type: "phone", show: { roles: ["admin+"] } },
internalNotes: { type: "redact", show: { roles: ["admin+"] } },
},
read: {
access: { roles: ["member+"] },
views: {
pipeline: {
fields: ["id", "name", "source"],
access: { roles: ["member+"] },
query: {
filterable: ["source"],
searchable: ["name"],
sortable: ["createdAt", "name"],
defaultSort: "-createdAt",
},
},
full: {
fields: ["id", "name", "email", "phone", "resumeUrl", "source", "internalNotes"],
access: { roles: ["admin+"] },
query: {
filterable: ["source", "email", "phone"],
searchable: ["name", "email", "internalNotes"],
sortable: ["createdAt", "name", "email"],
},
},
},
},
crud: {
create: { access: { roles: ["admin+"] } },
update: { access: { roles: ["admin+"] } },
delete: { access: { roles: ["owner"] }, mode: "soft" },
},
}); // candidates.resource.ts — security helpers
import { eq, and, isNull, type SQL } from 'drizzle-orm';
import { candidates } from './schema';
import { masks } from '../../lib/masks';
// Auto-derived from q.scope("organization") + audit-injected deletedAt.
// 2 predicates: tenant scope AND soft-delete filter.
export function buildFirewallConditions(ctx: AppContext): SQL | undefined {
return and(
eq(candidates.organizationId, ctx.activeOrgId!),
isNull(candidates.deletedAt),
);
}
// q.scope() + audit fields → systemManaged. Always rejected from client input.
export const GUARDS_CONFIG = {
createable: new Set(['name', 'email', 'phone', 'resumeUrl', 'source', 'internalNotes']),
updatable: new Set(['name', 'email', 'phone', 'resumeUrl', 'source', 'internalNotes']),
systemManaged: new Set([
'createdAt', 'createdBy', 'modifiedAt', 'modifiedBy',
'deletedAt', 'deletedBy',
'organizationId', // ← added by q.scope("organization")
]),
};
// Derived from masking[col].show.roles. Non-admins receive masked values.
export function maskCandidate<T extends Record<string, any>>(record: T, ctx: AppContext): T {
const masked: any = { ...record };
if (!ctx.roles?.some(r => ['admin', 'owner'].includes(r))) {
if (masked.email != null) masked.email = masks.email(masked.email);
if (masked.phone != null) masked.phone = masks.phone(masked.phone);
if (masked.internalNotes != null) masked.internalNotes = masks.redact(masked.internalNotes);
}
return masked;
}
// candidates.routes.ts — unified read pipeline (excerpt)
const VIEWS = {
pipeline: {
fields: ['id', 'name', 'source'],
access: { roles: ['member', 'admin', 'owner'] },
query: { filterable: ['source'], searchable: ['name'], sortable: ['createdAt', 'name'], defaultSort: '-createdAt' },
},
full: {
fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source', 'internalNotes'],
access: { roles: ['admin', 'owner'] },
// Masking query gate satisfied: full.access ∩ masking[*].show.roles = ['admin']
query: { filterable: ['source', 'email', 'phone'], searchable: ['name', 'email', 'internalNotes'], sortable: ['createdAt', 'name', 'email'] },
},
} as const;
// GET /candidates/views/:viewName — named view projection
app.get('/views/:viewName', async (c) => {
const ctx = c.get('ctx');
if (!ctx.authenticated) return c.json(AuthErrors.required(), 401);
const viewName = c.req.param('viewName');
const params = Object.fromEntries(new URL(c.req.url).searchParams);
const view = VIEWS[viewName as keyof typeof VIEWS];
if (!view) return c.json({ error: 'Unknown view', code: 'VIEW_NOT_FOUND' }, 400);
// Per-view access gate
if (!await evaluateAccess(view.access, ctx)) {
return c.json(AccessErrors.roleRequired(view.access.roles, ctx.roles), 403);
}
// Filter / sort / search are bounded by the view's query allowlist.
// Masked columns (email, phone, internalNotes) are excluded unless the
// view's query.{cap} explicitly lists them — validator already proved at
// compile time that the view's access roles satisfy masking.show.roles.
const filterCols = new Set(view.query.filterable ?? []);
const sortCols = new Set(view.query.sortable ?? []);
const searchCols = view.query.searchable ?? [];
const where: SQL[] = [];
const fw = buildFirewallConditions(ctx);
if (fw) where.push(fw);
for (const [k, v] of Object.entries(params)) {
const [field, op] = k.split('.');
if (!filterCols.has(field) || !(field in candidates)) continue;
const col = candidates[field as keyof typeof candidates];
where.push(op === 'like'
? sql`${col} LIKE ${'%' + escapeLike(v) + '%'} ESCAPE '\\'`
: eq(col, parseFilterValue(v)));
}
if (params.search && searchCols.length) {
const term = '%' + escapeLike(params.search) + '%';
where.push(or(...searchCols.map(c =>
sql`${candidates[c as keyof typeof candidates]} LIKE ${term} ESCAPE '\\'`,
))!);
}
// …pagination, sort, project view.fields, run query…
const rows = await db.select(/* projected fields */).from(candidates).where(and(...where));
return jsonWithEtag(c, { data: maskCandidates(rows, ctx), view: viewName });
}); BEYOND CRUD
Workflows the compiler enforces.
Define the workflow once. The compiler enforces who can transition, when, and which fields can change at each step — across REST, MCP, and the admin UI.
actions: {
sendWelcomeEmail: {
description: 'Send welcome email to new member',
input: z.object({
template: z.enum(['standard', 'vip']),
personalNote: z.string().optional()
}),
access: {
roles: ['admin', 'hr-manager'],
record: { status: { equals: 'active' } }
},
execute: async ({ db, ctx, record, input }) => {
await sendEmail({
to: record.email,
template: input.template,
from: ctx.user.name
});
return { emailSent: true, sentBy: ctx.user.id };
}
}
} Approvals & workflows
Multi-step operations like "approve invoice" or "close sprint" that enforce permissions and preconditions.
External integrations
Call any API — email providers, AI models, internal services — from typed Actions with full user context.
Webhooks & inbound events
Handle incoming webhooks as first-class operations, not one-off endpoints.
Queues & async work
Trigger background jobs, scheduled tasks, and fire-and-forget operations from any Action.
AI help is at the ready.
Install the CLI to access everything you need to define your backend.
Compiler CLI
Scaffold, compile, and deploy from the command line. Templates for every stack.
MCP Server
Connect any AI tool to the Quickback compiler. Schema-aware context for every prompt.
Claude Code Skills
AI-assisted backend development. Describe your feature, get a compiled API with security built in.
WHO IT'S FOR
Built for builders at every level
First-Time Builders
Your first backend doesn't have to be a security nightmare. Security is the default.
Learn more →Indie Developers
Ship your SaaS this weekend. Strong opinions, secure defaults, patterns that work.
Learn more →Supabase Users
Love Supabase? Keep it. Quickback compiles your security rules into RLS policies.
Learn more →CISOs & Enterprise
Let teams build. Compile-time governance makes "yes" possible.
Learn more →FAQ
Questions you're already asking
What happens when I need custom logic beyond CRUD?
That's what Actions are for. Define typed business operations - approve an invoice, stream an AI response, process a webhook - with the same security guarantees as your CRUD endpoints. Actions have full access to the database, user context, and any external service. There's no ceiling: if you can write it in TypeScript, you can ship it as an action.
Is there an escape hatch?
Several, by design. Actions cover anything beyond CRUD — your own typed handlers, wrapped in the same security layers. Every auto rule has an explicit exception — firewall.exception, access overrides, guard opt-outs — so a single resource can break a default without breaking the model. And the generated output is yours: patch it after compile, branch off entirely, or stop using Quickback and keep shipping with what you already have. The compiler is a build step, not a runtime — there's nothing to escape from.
What happens when I recompile? Do I lose my changes?
Recompiling overwrites the generated API code — and that's the point. Your definitions and actions are your source of truth. The generated src/ folder is output, like a build artifact. Change your definitions, recompile, and your entire API updates. Action handler files live in your project and are never overwritten — the compiler wraps them with authentication, access checks, and validation automatically.
How is this different from Supabase?
Quickback isn't competing with Supabase - the Compiler works with it, generating RLS from your definitions so you never write policies by hand. But if you want to own the whole stack, Quickback Stack gives you a complete Supabase-equivalent backend on your own Cloudflare account, and the Account UI gives you a ready-made auth frontend. Same definitions power all three.
Do I need Postgres? What databases are supported?
Quickback supports Cloudflare D1 (SQLite at the edge), Supabase Postgres (cloud or self-hosted), and Neon Postgres (serverless). The same definitions compile to different database targets - choose the one that fits your stack.
Am I locked into Quickback?
No. The output is standard TypeScript - Hono routes, Drizzle ORM, Better Auth. If you stop using Quickback tomorrow, you still have a working codebase built on well-known libraries. There's nothing proprietary in the generated code.
What if different roles need different fields?
Views are named field projections with role-based access control — column-level security. Define which fields each role can see, and the compiler generates a dedicated endpoint at /api/v1/{resource}/views/{viewName}. Views inherit all your security pillars — firewall, access, and masking — so a masked field stays masked even inside a view. Same pagination, filtering, and sorting as your list endpoint, just scoped to the fields you choose.
What is the Account UI?
A production-ready React SPA that plugs directly into your Quickback Stack or any Better Auth backend. Login, signup, passkeys, magic links, email OTP, organization management, role assignments, team creation, invitations, and an admin panel — out of the box. Deploy it as a Cloudflare Worker on your own subdomain. Fully customizable through environment variables.
How do Actions work across recompiles?
Actions are defined in your Quickback schema and the routes are regenerated on every compile — that's how they stay in sync with your security config. But your handler files are yours. They live in your project, the compiler never touches them. It just wraps them with authentication, access checks, input validation, and org scoping automatically. Write the business logic, Quickback handles the rest.
Join the Beta
We're onboarding early adopters now. Get hands-on access, direct support, and help shape the future of backend development.