Introduction
A thin, secure data layer between your frontend and any database.
One library to connect your frontend to any database -- with authentication, row-level permissions, and type safety built in.
What is superapp?
superapp is three packages:
| Package | What it does |
|---|---|
@superapp/backend | Connects to Postgres, MySQL, SQLite, or CSV through DuckDB. Handles auth, enforces row-level permissions, runs queries. |
@superapp/db | Drizzle ORM client with an HTTP driver. You write standard Drizzle queries — the data you get back is already filtered, restricted, and validated by the backend's permission engine. |
@superapp/auth | Client-side authentication. Uses better-auth by default, but supports custom adapters. Provides session management, React hooks, and pre-built auth UI components. |
┌───────────────────────────────────────────────────────────┐
│ YOUR FRONTEND (React, Next.js, etc.) │
│ │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ @superapp/db │ │ @superapp/auth │ │
│ │ Drizzle ORM │ │ Session management │ │
│ │ db.select(...) │ │ useSession() │ │
│ │ db.insert(...) │ │ AuthCard │ │
│ └─────────┬──────────┘ └───────────┬────────────┘ │
│ └──────────┬───────────────┘ │
└────────────────────────┼──────────────────────────────────┘
│
│ HTTP + JWT user token
▼
┌───────────────────────────────────────────────────────────┐
│ @superapp/backend │
│ │
│ 1. Authenticate ── verify JWT, resolve user + roles │
│ │ │
│ ▼ │
│ 2. Authorize ──── check permissions for this user │
│ │ • inject WHERE filters (user_id = ?) │
│ │ • restrict columns to allowed set │
│ │ • validate writes against rules │
│ ▼ │
│ 3. Execute ────── build SQL → run through DuckDB │
└───────────────────────────┬───────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Postgres MySQL SQLite / CSVEvery request from your frontend goes through this pipeline. You write normal Drizzle ORM queries — db.select(), db.insert(), etc. — but the backend intercepts each one, verifies who the user is, and automatically filters the data so that each user can only access their own data. The client never builds SQL and never bypasses the permission layer.
How it works
- Define your server -- connect databases, configure auth, declare permissions
- Generate types -- the CLI introspects your schema and outputs Drizzle table definitions
- Query from the frontend -- use standard Drizzle ORM syntax, permissions are enforced automatically
Quick Example
Server (server.ts):
import { createEngine } from '@superapp/backend'
import { betterAuthProvider } from '@superapp/backend/auth/better-auth'
import { postgresProvider } from '@superapp/backend/integrations/postgres'
import { createHonoMiddleware } from '@superapp/backend/adapters/hono'
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const engine = createEngine({
integrations: [postgresProvider],
connections: {
main: { type: 'postgres', url: process.env.PG_URL! },
},
auth: betterAuthProvider({ secret: process.env.AUTH_SECRET! }),
permissions: {
view_own_orders: {
table: 'main.orders',
operations: { select: true },
columns: ['id', 'amount', 'status', 'created_at'],
filter: { customer_id: { $eq: '$user.id' } },
},
},
roles: {
viewer: ['view_own_orders'],
},
})
const app = new Hono()
app.route('/', createHonoMiddleware(engine))
serve({ fetch: app.fetch, port: 3001 })Client (lib/superapp.ts):
import { drizzle } from '@superapp/db'
import { createAuth } from '@superapp/auth'
import * as schema from '../generated/schema'
export const authClient = createAuth('http://localhost:3001')
export function createDb(token: string) {
return drizzle({
connection: 'http://localhost:3001',
token,
schema,
})
}Query (anywhere in your frontend):
import { eq, desc } from 'drizzle-orm'
const orders = await db.select()
.from(schema.orders)
.where(eq(schema.orders.status, 'active'))
.orderBy(desc(schema.orders.createdAt))
.limit(50)What happens here: The client asks for active orders, but the filter: { customer_id: { $eq: '$user.id' } } in the server config automatically injects WHERE customer_id = ? into every query. The logged-in user only sees their own orders, filtered by active status — without the client writing any authorization logic. The backend enforces it in the middleware before any data leaves the server.
Because the authorization layer lives entirely in the backend middleware, you can safely use Drizzle ORM directly from the client side — the data returned is always scoped to the user's permissions. That said, we still recommend keeping Drizzle queries in your backend (e.g. Next.js server actions, API routes) for better control over caching, error handling, and request batching.
What's Next
- Installation -- Install the packages and set up your environment.
- Quick Start -- Get running in 5 minutes.