Adding Teenybase to an Existing Hono Project
You already have a Hono app on Cloudflare Workers and you want to add auth, CRUD, row-level security, an admin panel, and auto-generated migrations -- all driven by a single config file. This guide covers manual setup for full understanding. You can also run teeny init inside your existing project directory to auto-detect what's already set up and generate only the missing files.
Prerequisites
- An existing Hono project deployed on Cloudflare Workers
- Node.js >= 18.14.1
- Wrangler CLI installed
1. Install Teenybase
npm install teenybase2. Create a D1 Database
npx wrangler d1 create <your-database-name>Note the database_id from the output -- you need it in the next step.
3. Configure Wrangler Bindings
Add the D1 database binding to your wrangler.jsonc (or wrangler.toml):
{
"d1_databases": [
{
"binding": "PRIMARY_DB",
"database_name": "<your-database-name>",
"database_id": "<your-database-id>",
"migrations_dir": "migrations"
}
],
"vars": {
"RESPOND_WITH_ERRORS": "true" // Shows detailed errors in responses (disable in production)
}
}Enable Cloudflare Workers Logs for log streaming with teeny logs:
{
"observability": {
"enabled": true,
"logs": {
"invocation_logs": true,
"head_sampling_rate": 1
}
}
}If you need file storage (e.g., for user avatars), also add an R2 bucket binding:
{
"r2_buckets": [
{
"binding": "PRIMARY_R2",
"bucket_name": "<your-bucket-name>"
}
]
}After updating bindings, regenerate your type definitions:
npx wrangler types --env-interface CloudflareBindingsThis updates worker-configuration.d.ts to include PRIMARY_DB: D1Database (and PRIMARY_R2: R2Bucket if applicable).
4. Create the Database Config
Create teenybase.ts (or teeny.config.ts) in the project root. This single file defines your entire backend schema, auth, and access rules:
// teenybase.ts
import { DatabaseSettings, TableAuthExtensionData, TableRulesExtensionData } from "teenybase"
import { baseFields, authFields, createdTrigger } from "teenybase/scaffolds/fields"
export default {
appName: "My App",
appUrl: "https://myapp.com",
jwtSecret: "$JWT_SECRET_MAIN",
// Optional: allow external auth providers (e.g., Google OAuth + One Tap)
authProviders: [
{ name: 'google', clientId: '$GOOGLE_CLIENT_ID' },
],
tables: [
// Users table with authentication
{
name: "users",
autoSetUid: true,
fields: [
...baseFields, // id, created, updated
...authFields, // username, email, password, etc.
],
triggers: [createdTrigger],
extensions: [
{
name: "auth",
jwtSecret: "$JWT_SECRET_USERS",
jwtTokenDuration: 3600,
maxTokenRefresh: 5,
} as TableAuthExtensionData,
{
name: "rules",
listRule: "auth.uid == id",
viewRule: "auth.uid == id",
createRule: "true",
updateRule: "auth.uid == id",
deleteRule: "auth.uid == id",
} as TableRulesExtensionData,
],
},
// Example: Items table owned by users
{
name: "items",
autoSetUid: true,
fields: [
...baseFields,
{
name: "owner_id", type: "relation", sqlType: "text", notNull: true,
foreignKey: { table: "users", column: "id" },
},
{ name: "title", type: "text", sqlType: "text", notNull: true },
{ name: "content", type: "text", sqlType: "text" },
],
triggers: [createdTrigger],
extensions: [
{
name: "rules",
listRule: "auth.uid == owner_id",
viewRule: "auth.uid == owner_id",
createRule: "auth.uid != null & owner_id == auth.uid",
updateRule: "auth.uid == owner_id",
deleteRule: "auth.uid == owner_id",
},
],
},
],
// Optional: Email support via Resend
email: {
from: "My App <noreply@myapp.com>",
tags: ["backend"],
variables: {
company_name: "My App",
company_copyright: "My App Inc.",
company_address: "San Francisco, CA",
support_email: "support@myapp.com",
company_url: "https://myapp.com",
},
resend: {
RESEND_API_KEY: "$RESEND_API_KEY",
RESEND_WEBHOOK_SECRET: "$RESEND_WEBHOOK_SECRET",
},
},
} satisfies DatabaseSettingsValues prefixed with $ (e.g., $JWT_SECRET_MAIN) are resolved from .prod.vars (uploaded to Cloudflare secrets) or .dev.vars during local development. To expand this config:
- Getting Started: Add a Second Table -- adding tables with relations and rules
- Getting Started: Add Email, OAuth & More -- email verification, password reset, Google login
- Configuration Reference -- every option in teenybase.ts
5. Add TypeScript Path Alias
Teenybase uses a virtual module (virtual:teenybase) to load the config at build time. Add this path mapping to your tsconfig.json:
{
"compilerOptions": {
"paths": {
"virtual:teenybase": ["./teenybase"]
}
}
}6. Update Your Worker Entry Point
Modify your main src/index.ts to use teenyHono() instead of creating a bare Hono app. This wraps your app with Teenybase middleware that initializes the database on each request.
import { Hono } from "hono";
import { $Database, $Env, OpenApiExtension, PocketUIExtension, teenyHono } from "teenybase/worker";
import config from "virtual:teenybase";
// Merge Teenybase env types with your existing bindings
export interface Env {
Bindings: $Env["Bindings"] & CloudflareBindings;
Variables: $Env["Variables"];
}
// Create the Hono app with Teenybase middleware
const app = teenyHono<Env>(
async (c) => {
const db = new $Database(c, config, c.env.PRIMARY_DB /*, c.env.PRIMARY_R2 */);
// Optional: OpenAPI/Swagger UI at /api/v1/docs
db.extensions.push(new OpenApiExtension(db, true));
// Optional: Admin UI at /api/v1/pocket
db.extensions.push(new PocketUIExtension(db));
return db;
},
undefined,
{
cors: {
origin: [
"http://localhost:3000",
"http://localhost:5173",
"https://myapp.com",
],
allowHeaders: ["*"],
allowMethods: ["POST", "GET", "OPTIONS", "PUT", "DELETE", "PATCH"],
exposeHeaders: ["*"],
maxAge: 600,
credentials: true,
},
}
);
// Your existing routes continue to work below
app.get("/hello", (c) => c.text("Hello!"));
export default app;Key points:
teenyHono()creates a Hono app with CORS, logging, and error handling middleware pre-configured.- The
createDbcallback runs per-request, initializing$Databasewith your config and D1 binding. - Your existing routes still work -- add them after the
teenyHono()call. - R2 storage -- pass an R2 bucket as the fourth argument to
$Databaseif you need file storage.
Using an Existing Hono App
teenyHono() accepts an existing Hono app as its second parameter. Instead of creating a new app, pass yours in -- Teenybase adds its middleware (CORS, logging, error handling, $Database init) to it:
import { Hono } from "hono"
import { $Database, $Env, teenyHono } from "teenybase/worker"
import config from "virtual:teenybase"
const app = new Hono<Env>()
// Your existing routes
app.get("/", (c) => c.text("Home"))
app.get("/health", (c) => c.json({ ok: true }))
// Add teenybase middleware and routes to your app
teenyHono<Env>(async (c) => {
return new $Database(c, config, c.env.PRIMARY_DB)
}, app)
export default appYour routes and Teenybase's /api/v1/* routes coexist on the same app.
Mounting Under a Sub-Path
If you want Teenybase at a custom prefix (e.g., /backend/api/v1/... instead of /api/v1/...), mount the Teenybase app using Hono's app.route():
import { Hono } from "hono"
import { $Database, $Env, teenyHono } from "teenybase/worker"
import config from "virtual:teenybase"
const teenyApp = teenyHono<Env>(async (c) => {
return new $Database(c, config, c.env.PRIMARY_DB)
})
const app = new Hono<Env>()
app.get("/", (c) => c.text("Home"))
app.route("/backend", teenyApp) // teenybase API at /backend/api/v1/...
export default appTeenybase handles the prefix automatically -- all internal routing resolves correctly regardless of the mount path.
7. Set Up Secrets
Create a .dev.vars file for local development (this file should be in .gitignore):
JWT_SECRET_MAIN=your_main_jwt_secret
JWT_SECRET_USERS=your_users_jwt_secret
ADMIN_SERVICE_TOKEN=your_admin_service_token
ADMIN_JWT_SECRET=your_admin_jwt_secret
POCKET_UI_VIEWER_PASSWORD=viewer
POCKET_UI_EDITOR_PASSWORD=editorNote: apiRoute is stored in infra.jsonc (CLI project config, committed to git) rather than in secrets files. It is auto-saved when you deploy.
Add more variables as needed (e.g., GOOGLE_CLIENT_ID, RESEND_API_KEY) -- see Getting Started: Environment Variables.
For production, create a .prod.vars file with production values and upload them:
teeny secrets --remote --uploadnpx teeny secrets --remote --upload8. Add Package Scripts
Add Teenybase CLI scripts to your package.json (see CLI Reference for all commands and options):
{
"scripts": {
"generate": "teeny generate --local",
"migrate": "teeny deploy --local",
"backup:local": "teeny backup --local",
"dev": "teeny dev",
"build:local": "teeny build --local",
"exec": "teeny exec --local",
"deploy": "teeny deploy --remote --log-level debug",
"secrets-upload": "teeny secrets --remote --upload",
"build": "teeny build --remote",
"backup": "teeny backup --remote",
"generate:remote": "teeny generate --remote",
"migrate:remote": "teeny deploy --remote",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
}
}9. Update .gitignore
Add Teenybase working directories to .gitignore:
# Teenybase
.local-persist
.teeny
.tmp.*.json
.prod.vars
.prod.vars*
migrations10. Generate Migrations and Run
With everything configured, generate and apply your initial migrations:
teeny generate --local
teeny deploy --local
teeny dev --localnpx teeny generate --local
npx teeny deploy --local
npx teeny dev --localFor the first remote deployment:
teeny secrets --remote --upload
teeny deploy --remotenpx teeny secrets --remote --upload
npx teeny deploy --remoteAPI Endpoints
See API Endpoints for the full endpoint reference. Key routes once running:
| Endpoint | Description |
|---|---|
GET /api/v1/health | Health check |
GET /api/v1/doc/ui | Swagger UI (if OpenApiExtension enabled) |
GET /api/v1/pocket/ | Admin panel (if PocketUIExtension enabled) |
GET /api/v1/table/{table}/select | Query records |
GET /api/v1/table/{table}/list | Query with total count |
GET /api/v1/table/{table}/view/{id} | View single record |
POST /api/v1/table/{table}/insert | Create record |
POST /api/v1/table/{table}/update | Update records |
POST /api/v1/table/{table}/delete | Delete records |
POST /api/v1/table/{table}/auth/sign-up | Register |
POST /api/v1/table/{table}/auth/login-password | Login |
POST /api/v1/table/{table}/auth/refresh-token | Refresh JWT |
Summary of Changes
| File | Change |
|---|---|
package.json | Added teenybase dependency and CLI scripts |
wrangler.jsonc | Added D1 database binding and RESPOND_WITH_ERRORS var |
worker-configuration.d.ts | Regenerated to include PRIMARY_DB |
tsconfig.json | Added virtual:teenybase path alias |
teenybase.ts | New -- database schema and config |
src/index.ts | Replaced new Hono() with teenyHono() and $Database setup |
.dev.vars | New -- local secrets |
.gitignore | Added Teenybase working directories |