superapp

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:

PackageWhat it does
@superapp/backendConnects to Postgres, MySQL, SQLite, or CSV through DuckDB. Handles auth, enforces row-level permissions, runs queries.
@superapp/dbDrizzle ORM client built on Drizzle Proxy. You write standard Drizzle queries — the data you get back is already filtered, restricted, and validated by the backend's permission engine.
@superapp/authClient-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 Proxy      │  │  Session management     │    │
  │   │  db.select(...)     │  │  useSession()           │    │
  │   │  db.insert(...)     │  │  AuthCard               │    │
  │   └─────────┬──────────┘   └───────────┬────────────┘     │
  │             └──────────┬───────────────┘                  │
  └────────────────────────┼──────────────────────────────────┘

                           │ SQL + params + JWT (Drizzle Proxy)

  ┌───────────────────────────────────────────────────────────┐
  │  @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 ────── run permission-filtered SQL via DuckDB  │
  └───────────────────────────┬───────────────────────────────┘

                ┌─────────────┼─────────────┐
                ▼             ▼             ▼
             Postgres      MySQL       SQLite / CSV

Every request from your frontend goes through this pipeline. You write normal Drizzle ORM queries — db.select(), db.insert(), etc. — and the Drizzle Proxy driver sends the parameterized SQL to the backend. The server verifies who the user is, applies permission filters to the SQL, and automatically scopes the data so that each user can only access their own data.

How it works

  1. Define your server -- connect databases, configure auth, declare permissions
  2. Generate types -- the CLI introspects your schema and outputs Drizzle table definitions
  3. 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'],

      // ⬇️ THIS IS THE KEY LINE — it scopes every query to the logged-in user.
      // '$user.id' is replaced at runtime with the authenticated user's ID.
      // The backend injects: WHERE customer_id = '<current user id>'
      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'

// Looks like a normal Drizzle query — but the backend automatically
// injects WHERE customer_id = <logged-in user's id> before executing.
// User A only gets User A's orders. User B only gets User B's.
const orders = await db.select()
  .from(schema.orders)
  .where(eq(schema.orders.status, 'active'))
  .orderBy(desc(schema.orders.createdAt))
  .limit(50)

// ✅ Alice sees her orders:
//    [{ id: 'ord_1', amount: 49.99, status: 'active', created_at: '2025-01-15' },
//     { id: 'ord_3', amount: 149.00, status: 'active', created_at: '2025-01-10' }]
//
// ✅ Bob sees his orders (completely different rows, same query):
//    [{ id: 'ord_7', amount: 89.50, status: 'active', created_at: '2025-01-14' }]
//
// ❌ Alice can NEVER see Bob's orders — the backend enforces it

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.

The same Drizzle code, different results per user. Two users running the exact same db.select() query will get completely different rows — each scoped to their own data. You never write WHERE user_id = currentUser.id in your frontend. The backend handles it.

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

On this page