Configuration Reference
Teenybase is configured through a single TypeScript object in teenybase.ts (or teeny.config.ts) that satisfies the DatabaseSettings type. This page documents every option, every type, and every field.
Quick Reference: Field Types
| You're storing... | type | sqlType | Example |
|---|---|---|---|
| Text (name, title, bio) | 'text' | 'text' | { name: 'title', type: 'text', sqlType: 'text' } |
| A number (price, score) | 'number' | 'real' | { name: 'price', type: 'number', sqlType: 'real' } |
| A whole number (count, age) | 'integer' | 'integer' | { name: 'age', type: 'integer', sqlType: 'integer' } |
| True/false | 'bool' | 'boolean' | { name: 'active', type: 'bool', sqlType: 'boolean' } |
| An email address | 'email' | 'text' | { name: 'email', type: 'email', sqlType: 'text' } |
| A URL | 'url' | 'text' | { name: 'website', type: 'url', sqlType: 'text' } |
| A date or timestamp | 'date' | 'timestamp' | { name: 'due', type: 'date', sqlType: 'timestamp' } |
| JSON data | 'json' | 'json' | { name: 'config', type: 'json', sqlType: 'json' } |
| A file (image, PDF) | 'file' | 'text' | { name: 'avatar', type: 'file', sqlType: 'text' } |
| A link to another table | 'relation' | 'text' | { name: 'author_id', type: 'relation', sqlType: 'text', foreignKey: { table: 'users', column: 'id' } } |
| A dropdown / enum | 'select' | 'text' | { name: 'status', type: 'select', sqlType: 'text' } |
| Rich text / HTML | 'editor' | 'text' | { name: 'content', type: 'editor', sqlType: 'text' } |
For the full list with all compatible sqlType options, see Field Types below.
DatabaseSettings (top-level)
export default {
appUrl: 'http://localhost:8787',
jwtSecret: '$JWT_SECRET',
tables: [],
// Optional
appName: 'My App',
jwtIssuer: 'my-app',
jwtAlgorithm: 'HS256',
authProviders: [],
authCookie: {},
email: {},
actions: [],
version: 1,
} satisfies DatabaseSettings| Property | Type | Required | Default | Description |
|---|---|---|---|---|
appUrl | string | Yes | — | Your app's URL. Used for OAuth redirects, email links, and CORS. |
jwtSecret | string | Yes | — | Global JWT signing secret. Combined with table-level jwtSecret (concatenated) for table auth tokens. Prefix with $ to resolve from env vars (e.g., '$JWT_SECRET'). |
tables | TableData[] | Yes | — | Array of table definitions. Each becomes a SQLite table with REST endpoints. |
appName | string | No | 'Teeny App' | Application name, used in email templates as the APP_NAME variable. |
jwtIssuer | string | No | '$db' | JWT iss claim value for tokens created by this instance. |
jwtAlgorithm | string | No | 'HS256' | JWT signing algorithm. |
authProviders | AuthProvider[] | No | [] | External auth provider configurations — OAuth redirect flows, JWT verification, or both. See Auth Providers. |
allowedRedirectUrls | string[] | No | — | Allowed redirect URLs after OAuth login (exact match). When not set, URLs matching appUrl hostname are allowed. See OAuth Guide. |
authCookie | AuthCookieConfig | No | — | Cookie-based auth for SSR and OAuth flows. See Auth Cookie. |
email | EmailSettings | No | — | Email provider config for verification/reset emails. See Email Configuration. |
actions | SQLAction[] | No | [] | Server-side actions callable via API. See Actions Guide. |
version | number | No | — | Schema version number, tracked across migrations. |
Table Definition (TableData)
{
name: 'posts',
fields: [/* ... */],
// Optional
autoSetUid: true,
extensions: [/* ... */],
triggers: [/* ... */],
indexes: [/* ... */],
fullTextSearch: {/* ... */},
r2Base: 'posts',
idInR2: false,
autoDeleteR2Files: true,
allowMultipleFileRef: false,
allowWildcard: false,
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Table name. Used in API routes: /api/v1/table/{name}/. Must be a valid SQL identifier. |
fields | TableFieldData[] | Yes | — | Column definitions. See Field Definition. |
autoSetUid | boolean | No | false | Auto-generate a unique ID for the record_uid field on insert. Requires a field with usage: 'record_uid' and type: 'text'. |
extensions | TableExtensionData[] | No | [] | Extensions to enable: auth, rules, crud. CRUD is auto-included. |
triggers | SQLTrigger[] | No | [] | SQL triggers. See Triggers. |
indexes | SQLIndex[] | No | [] | Database indexes. See Indexes. |
fullTextSearch | object | No | — | Full-text search config. See Full-Text Search. |
r2Base | string | No | — | R2 bucket path prefix for file storage. Required if any field has type: 'file'. |
idInR2 | boolean | No | false | Store files under {r2Base}/{recordId}/ subdirectories in R2. Cannot be used with allowMultipleFileRef. Requires a record_uid field with noUpdate: true. |
autoDeleteR2Files | boolean | No | true | Automatically delete R2 files when records are deleted. |
allowMultipleFileRef | boolean | No | false | Allow referencing the same file from multiple records. Cannot be used with idInR2. Requires autoDeleteR2Files: false. |
allowWildcard | boolean | No | false | Allow * wildcard in select queries. |
Field Definition (TableFieldData)
{
name: 'title',
type: 'text',
sqlType: 'text',
// Optional — constraints
primary: false,
autoIncrement: false,
unique: false,
notNull: false,
default: sql`CURRENT_TIMESTAMP`,
check: sql`json_valid(config)`,
collate: 'NOCASE',
// Optional — foreign key
foreignKey: {
table: 'users',
column: 'id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
// Optional — API behavior
usage: 'record_uid',
noSelect: false,
noInsert: false,
noUpdate: false,
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Column name. Must start with a letter or underscore; alphanumeric + underscores only. |
type | string | Yes | — | Teenybase field type. Controls API-level behavior. See Field Types. |
sqlType | string | Yes | — | SQLite column type. Controls storage. See SQL Types. |
primary | boolean | No | false | PRIMARY KEY constraint. |
autoIncrement | boolean | No | false | AUTOINCREMENT (integer primary keys only). |
unique | boolean | No | false | UNIQUE constraint. |
notNull | boolean | No | false | NOT NULL constraint. |
default | SQLLiteral | string | No | — | Default value. Use sqlValue('text') for literal values or sql\CURRENT_TIMESTAMP`` for SQL expressions. |
check | SQLLiteral | string | No | — | CHECK constraint. Example: sql`json_valid(config)`. |
collate | string | No | — | Collation. 'BINARY', 'NOCASE', or 'RTRIM'. |
foreignKey | FieldForeignKey | No | — | Foreign key relation. See below. |
usage | string | No | — | Semantic usage that extensions act on. See Field Usages. |
noSelect | boolean | No | false | Exclude from API responses (hidden field). Used for passwords, salts, etc. |
noInsert | boolean | No | false | Cannot be set via API insert. Used for auto-generated fields. |
noUpdate | boolean | No | false | Cannot be changed via API update. Used for IDs, creation timestamps. |
Foreign Key Options
foreignKey: {
table: 'users', // Referenced table name
column: 'id', // Referenced column name
onDelete: 'CASCADE', // Action on parent delete
onUpdate: 'CASCADE', // Action on parent update
}| Action | Description |
|---|---|
'CASCADE' | Delete/update child rows when parent changes. |
'SET NULL' | Set foreign key to NULL when parent changes. |
'SET DEFAULT' | Set foreign key to default when parent changes. |
'RESTRICT' | Prevent parent change if children exist. |
'NO ACTION' | Same as RESTRICT in SQLite. |
Field Types
The type property controls how teenybase treats the field at the API level — validation, serialization, and extension behavior.
| Type | Description | Compatible sqlTypes |
|---|---|---|
text | Plain text string | text, null |
number | Floating-point number | integer, real |
integer | Integer number | integer |
bool | Boolean (true/false) | boolean, integer |
email | Email address (validated format) | text |
url | URL string | text |
editor | Rich text / HTML content | text |
date | Date or datetime value | text, timestamp, datetime, date, time |
select | Enum / dropdown value | text, integer, real |
json | JSON object or array | text, json |
file | File reference (stored in R2) | text |
relation | Foreign key reference | text, integer, real |
password | Password field (hashed) | text |
blob | Binary data | blob |
SQL Types
The sqlType property maps to the actual SQLite column type used in DDL.
| sqlType | SQLite Affinity | Notes |
|---|---|---|
text | TEXT | Most common. Strings, JSON, file paths, relations. |
integer | INTEGER | Integers, booleans (0/1). |
real | REAL | Floating-point numbers. |
boolean | NUMERIC | Stored as 0/1 in SQLite. |
blob | BLOB | Binary data. |
json | TEXT | Stored as JSON string. Can use json_valid() CHECK constraint. |
timestamp | TEXT | ISO 8601 datetime string. Used with CURRENT_TIMESTAMP. |
datetime | TEXT | Alias for timestamp. |
date | TEXT | Date-only string. |
time | TEXT | Time-only string. |
float | REAL | Alias for real. |
int | INTEGER | Alias for integer. |
numeric | NUMERIC | SQLite numeric affinity. |
null | — | NULL type (rare). |
Field Usages
Usages give fields semantic meaning. Extensions detect usages automatically and wire up behavior — you don't need to write any handler code.
Record Usages
| Usage | Description | Auto behavior |
|---|---|---|
record_uid | Unique record identifier | Auto-generated on insert (when autoSetUid: true). Used as the record's primary identifier in API responses. |
record_created | Creation timestamp | Set to CURRENT_TIMESTAMP on insert. Protected from updates via trigger. |
record_updated | Last update timestamp | Set to CURRENT_TIMESTAMP on every update via trigger. |
Auth Usages
These are recognized by the Auth Extension and enable automatic authentication behavior.
| Usage | Description | Auto behavior |
|---|---|---|
auth_email | User's email address | Used as login identity. Must be unique. |
auth_username | Username | Used as alternative login identity. Must be unique. |
auth_password | Password hash | Automatically hashed on sign-up. Hidden from API responses (noSelect). |
auth_password_salt | Password salt | Auto-generated. Hidden from API responses. |
auth_email_verified | Email verification status | Set to false on sign-up. Updated on verification confirm. |
auth_name | Display name | Included in JWT payload. |
auth_avatar | Profile picture (file) | Stored in R2 via file upload. |
auth_audience | Role / audience | Included in JWT aud claim. Used for role-based access. |
auth_metadata | User metadata (JSON) | Included in JWT meta claim. Flexible key-value storage. |
Extensions
Auth Extension (TableAuthExtensionData)
Adds authentication endpoints to a table — sign-up, login, password reset, email verification, OAuth, and JWT management.
{
name: 'auth',
jwtSecret: '$JWT_SECRET_USERS', // required
jwtTokenDuration: 3600, // required (seconds)
maxTokenRefresh: 5, // required (0 = unlimited)
} as TableAuthExtensionDataJWT (required):
| Property | Type | Default | Description |
|---|---|---|---|
jwtSecret | string | Required | Table-level JWT secret. Concatenated with global jwtSecret to form the signing key. Use $ prefix for env vars. |
jwtTokenDuration | number | Required | JWT access token expiry in seconds. |
maxTokenRefresh | number | Required | Max refreshes before re-login. Set to 0 for unlimited. |
Passwords:
| Property | Type | Default | Description |
|---|---|---|---|
passwordType | 'sha256' | 'sha256' | Password hashing algorithm. Currently only sha256 supported. |
passwordConfirmSuffix | string | — | Suffix for password confirmation field (e.g., 'Confirm' → passwordConfirm). When set, sign-up requires both fields to match. |
passwordCurrentSuffix | string | 'Current' | Suffix for current password field on update (e.g., passwordCurrent). Set to '' to disable (not recommended). |
Email and verification:
| Property | Type | Default | Description |
|---|---|---|---|
autoSendVerificationEmail | boolean | false | Send verification email on sign-up. Requires email service config. |
normalizeEmail | boolean | true | Lowercase, trim, punycode domains, provider-specific rules (gmail: remove dots/plus-addressing). |
passwordResetTokenDuration | number | 3600 | Reset token validity in seconds. |
emailVerifyTokenDuration | number | 3600 | Verification token validity in seconds. |
passwordResetEmailDuration | number | 120 | Min interval between reset emails (seconds). |
emailVerifyEmailDuration | number | 120 | Min interval between verification emails (seconds). |
emailTemplates | object | — | Custom templates for verification and passwordReset. See below. |
OAuth:
| Property | Type | Default | Description |
|---|---|---|---|
saveIdentities | boolean | false | Save OAuth provider identity data in _auth_identities table. |
Email Templates
emailTemplates: {
verification: {
subject: 'Verify your email',
layoutHtml: '<div>{{EMAIL_CONTENT}}</div>', // custom inner template (wrapped in base + message layouts)
variables: { custom_key: 'value' },
tags: 'verification',
},
passwordReset: {
subject: 'Reset your password',
layoutHtml: ['<html>{{EMAIL_CONTENT}}</html>', '<div>{{EMAIL_CONTENT}}</div>', '<p>Reset</p>'], // full template stack
},
}| Property | Type | Description |
|---|---|---|
subject | string | Email subject line. Supports placeholders. |
layoutHtml | string | string[] | Custom email layout. A full HTML document (starts with <html or <!DOCTYPE) is used as-is. A fragment string is wrapped in the default base and message layouts. An array of strings replaces the entire template stack — each entry nests into the previous via . |
variables | Record<string, any> | Additional template variables merged into the email. |
tags | string | Tags for email tracking (provider-dependent). |
The default email templates (baseLayout1, messageLayout1, actionLinkTemplate, actionTextTemplate) are exported from teenybase/worker for use in custom layouts.
Endpoints added: See API Endpoints
JWT Claims
Auth tokens issued by teenybase contain these claims (defined in src/types/jwt.ts):
| Claim | Source field usage | Description |
|---|---|---|
id | record_uid | User's unique record ID |
sub | auth_email | User's email address |
user | auth_username | User's username (note: claim is user, not username) |
aud | auth_audience | User's role(s) — only included if non-empty |
verified | auth_email_verified | Whether email is verified — only included if field exists |
meta | auth_metadata | Custom metadata (JSON) — only included if field exists |
cid | — | Table name the token was issued for (e.g. "users") |
sid | — | Session ID (for refresh token validation) |
iat | — | Issued at (Unix timestamp) |
exp | — | Expires at (Unix timestamp) |
iss | — | Issuer (from DatabaseSettings.jwtIssuer, default '$db') |
How JWT signing works: The signing key is globalJwtSecret + tableJwtSecret (concatenated). Both resolve $-prefixed env vars at runtime.
Rules Extension (TableRulesExtensionData)
Row-level security via expression-based access rules. Rules compile to SQL WHERE clauses and are injected into every query.
{
name: 'rules',
listRule: 'auth.uid == owner_id',
viewRule: 'auth.uid == owner_id',
createRule: 'auth.uid != null',
updateRule: 'auth.uid == owner_id',
deleteRule: 'auth.uid == owner_id',
}| Property | Type | Default | Description |
|---|---|---|---|
listRule | string | null | null | Filter for list/select queries. Converted to SQL WHERE clause. null blocks all access (403). |
viewRule | string | null | null | Filter for single-record view. Converted to SQL WHERE clause. null blocks all access (403). |
createRule | string | null | null | Guard for insert operations. Evaluated against new record values at parse time. null blocks all access (403). |
updateRule | string | null | null | Filter for update operations. Converted to SQL WHERE clause. null blocks all access (403). |
deleteRule | string | null | null | Filter for delete operations. Converted to SQL WHERE clause. null blocks all access (403). |
Rule Values
| Value | Behavior |
|---|---|
'true' | Allow all requests, including unauthenticated. |
'false' | Deny all requests. |
null or omitted | Deny all requests (no rule = no access). |
| Expression string | Evaluated per request. See below. |
Expression Syntax
Rules use a JS-like expression language that compiles to parameterized SQL WHERE clauses. All string/number literals in expressions become ? placeholders with bound values, and column names are validated against the table schema. This means string interpolation in expressions (e.g., where: `email == '${userInput}'`) is safe — it's the intended ORM-like query pattern, not raw SQL.
Available variables:
| Variable | Description |
|---|---|
auth.uid | Authenticated user's ID. null if not logged in. |
auth.email | User's email from JWT. |
auth.role | User's role/audience from JWT aud claim. |
auth.verified | Whether user's email is verified (boolean). |
auth.admin | Whether user is admin (boolean). |
auth.superadmin | Whether user is superadmin (boolean). |
auth.meta | User metadata from JWT meta claim. |
auth.jwt | Raw JWT payload (all claims). |
| Column names | Any column from the current table (e.g., owner_id, published). Not available in createRule. |
new.* | New values being inserted/updated (e.g., new.status, new.author_id). Available in createRule and updateRule only. |
Operators:
| Operator | SQL Equivalent | Example |
|---|---|---|
== | IS | auth.uid == id |
!= | IS NOT | auth.uid != null |
= | = | role = 'guest' |
> | > | price > 0 |
< | < | age < 18 |
>= | >= | priority >= 5 |
<= | <= | count <= 100 |
~ | LIKE | email ~ '%@example.com' |
!~ | NOT LIKE | role !~ '%admin' |
! | NOT (unary) | !deleted_at |
& | AND | auth.uid == id & published == true |
| | OR | role == 'admin' | role == 'editor' |
|| | || (concat) | path ~ (auth.meta.base || '%') |
@@ | MATCH (FTS5) | articles @@ 'search term' |
SQL functions available in expressions:
| Function | Description | Example |
|---|---|---|
lower(x) | Lowercase | lower(email) == 'admin@test.com' |
upper(x) | Uppercase | upper(status) == 'ACTIVE' |
length(x) | String length | length(name) > 3 |
substring(x, start, len) | Substring | substring(code, 1, 2) == 'US' |
replace(x, from, to) | Replace | replace(name, ' ', '-') |
concat(a, b, ...) | Concatenate | concat(first, ' ', last) |
count(x), sum(x) | Aggregates | SELECT/actions only |
datetime(x), date(x), time(x) | Date/time | date(created) == date('now') |
unixepoch(x) | Unix timestamp | unixepoch('now') - unixepoch(created) < 3600 |
json_set, json_insert, json_replace, json_patch | JSON mutation | json_set(meta, '$.key', 'value') |
json_contains(json, val) | JSON array membership | json_contains(tags, 'important') |
Rule Examples
// Public read, authenticated write
{
name: 'rules',
listRule: 'true', // Anyone can list
viewRule: 'true', // Anyone can view
createRule: 'auth.uid != null', // Must be logged in to create
updateRule: 'auth.uid == owner_id', // Only the owner can update
deleteRule: 'auth.uid == owner_id', // Only the owner can delete
}
// Admin-only table
{
name: 'rules',
listRule: "auth.role == 'admin'",
viewRule: "auth.role == 'admin'",
createRule: "auth.role == 'admin'",
updateRule: "auth.role == 'admin'",
deleteRule: "auth.role == 'admin'",
}
// Users can only see/edit their own records
{
name: 'rules',
listRule: 'auth.uid == id',
viewRule: 'auth.uid == id',
createRule: "auth.uid == null & role = 'guest'", // Sign-up: only non-authenticated, default role
updateRule: 'auth.uid == id',
deleteRule: null, // No one can delete
}CRUD Extension
Auto-included for every table. Provides REST endpoints for CRUD operations.
{ name: 'crud' }No additional configuration. The CRUD extension registers these routes:
| Method | Path | Description |
|---|---|---|
| GET/POST | /select | Query records |
| GET/POST | /list | Query records with total count |
| GET | /view/:id | Get single record by ID |
| POST | /insert | Insert one or more records |
| POST | /update | Update records by filter |
| POST | /edit/:id | Update a single record by ID |
| POST | /delete | Delete records by filter |
See API Endpoints for full request/response formats.
OpenAPI Extension
Auto-generates an OpenAPI 3.1.0 spec from your tables, extensions, and actions.
import { OpenApiExtension } from 'teenybase/worker'
db.extensions.push(new OpenApiExtension(db)) // with Swagger UI (default)
db.extensions.push(new OpenApiExtension(db, false)) // JSON spec only, no Swagger UI| Parameter | Type | Default | Description |
|---|---|---|---|
db | $Database | (required) | The database instance |
swagger | boolean | true | Enable Swagger UI at /doc/ui |
Routes (under /api/v1):
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/doc | OpenAPI 3.1.0 JSON spec |
| GET | /api/v1/doc/ui | Swagger UI (if swagger is true) |
Notes:
- The document title is "Teenybase API" (v1.0.0) — not configurable.
- Routes with incompatible zod schemas (e.g.,
superRefine) are silently skipped with a console warning. - An
authorizationheader parameter is auto-injected into every route's spec.
PocketUI Extension
Built-in admin panel with table browsing, record editing, R2 storage management, usage dashboard, and worker log viewer.
PocketUIExtension consolidates the admin UI, storage browser, usage dashboard, and logs into a single extension. It operates in two modes: managed (via Teenybase Cloud) and direct (self-hosted on your own Cloudflare account).
Managed mode (Teenybase Cloud)
If you deploy via teeny deploy --remote through Teenybase Cloud, PocketUI works out of the box with no config:
import { PocketUIExtension } from 'teenybase/worker'
db.extensions.push(new PocketUIExtension(db))Logs and usage data are fetched from the Teenybase platform automatically using your PLATFORM_WORKER_TOKEN.
Direct mode (self-hosted)
If you deploy to your own Cloudflare account, pass a PocketUiConfig object with mode: 'direct':
import { PocketUIExtension } from 'teenybase/worker'
db.extensions.push(new PocketUIExtension(db, {
mode: 'direct',
workerName: 'my-worker',
usageResources: {
workers: [{ name: 'my-worker', cfId: 'my-worker' }],
d1: [{ name: 'my-db', cfId: '<d1-database-uuid>' }],
r2: [{ name: 'my-bucket', cfId: 'my-bucket' }],
},
}))Add CF_API_TOKEN and CF_ACCOUNT_ID to .dev.vars and .prod.vars. Without these, the usage dashboard and log viewer return errors.
Config reference
| Field | Type | Default | Description |
|---|---|---|---|
mode | 'managed' | 'direct' | — | managed for Teenybase Cloud, direct for self-hosted |
workerName | string | auto-derived in managed mode | Worker script name for log queries. Required in direct mode. |
baseUrl | string | CDN (jsdelivr.net/npm/@teenybase/pocket-ui@.../dist/) | Custom URL for UI assets (e.g., https://teeny-pocket-ui.pages.dev/ or http://localhost:4173/ for local development) |
uiVersion | string | 'latest' | Version of @teenybase/pocket-ui npm package (only used with CDN baseUrl) |
platformUrl | string | PLATFORM_BASE_URL env | Managed mode only. Teenybase platform origin for usage/logs API calls. |
usageResources | object | USAGE_RESOURCES env (JSON) | Direct mode only. Declares which CF resources to query for the usage dashboard. See below. |
usageResources shape:
{
workers: [{ name: string, cfId: string }], // cfId = script name
d1: [{ name: string, cfId: string }], // cfId = D1 database UUID
r2: [{ name: string, cfId: string }], // cfId = bucket name
}You can also set this via the USAGE_RESOURCES environment variable as a JSON string instead of hardcoding it in the config.
Routes
All routes are under /api/v1.
Admin UI & auth:
| Method | Path | Description |
|---|---|---|
| GET | /pocket/ | Admin panel UI (proxied from CDN or custom baseUrl) |
| GET | /pocket/login | Login page |
| POST | /pocket/login | Authenticate with username + password |
| GET | /pocket/logout | Clear session cookies, redirect to login |
Storage browser (requires R2 bucket binding):
| Method | Path | Description |
|---|---|---|
| GET | /storage/list | List objects in R2 bucket (supports prefix, cursor, limit, delimiter query params) |
| GET | /storage/object/:key | Download an object |
| PUT | /storage/object/:key | Upload an object (editor+ role required) |
| DELETE | /storage/object/:key | Delete an object (editor+ role required) |
Usage dashboard:
| Method | Path | Description |
|---|---|---|
| GET | /pocket/usage?range=24h | Usage data with cost breakdown. Ranges: 1h, 6h, 24h, 7d, 30d. |
Log viewer (only available when workerName is resolved):
| Method | Path | Description |
|---|---|---|
| GET | /logs/workers | List workers available for log streaming |
| POST | /logs/query | Query worker telemetry logs |
Environment variables
| Variable | Mode | Description |
|---|---|---|
POCKET_UI_VIEWER_PASSWORD | both | Password for viewer role (read-only) |
POCKET_UI_EDITOR_PASSWORD | both | Password for editor role (read + write) |
ADMIN_SERVICE_TOKEN | both | Also accepted as superadmin password |
CF_API_TOKEN | direct | Cloudflare API token with Workers/D1/R2 read permissions |
CF_ACCOUNT_ID | direct | Your Cloudflare account ID |
USAGE_RESOURCES | direct | JSON alternative to usageResources config field |
PLATFORM_WORKER_TOKEN | managed | Auto-set by Teenybase Cloud |
PLATFORM_BASE_URL | managed | Auto-set by Teenybase Cloud |
Cookie behavior
- Session cookies expire after 1 hour
teeny-pocket-ui-access-token— signed, httpOnly (auth token)teeny-pocket-ui-user-data— non-httpOnly (user info for the UI frontend)SameSite: Strict,Secureonly on HTTPS
Scaffolds
Pre-built field and trigger definitions importable from teenybase/scaffolds/fields. These save boilerplate for common table patterns.
import { baseFields, authFields, createdTrigger, updatedTrigger } from 'teenybase/scaffolds/fields'baseFields
Three fields every table should have: a unique ID, creation timestamp, and update timestamp.
| Field | type | sqlType | usage | Constraints |
|---|---|---|---|---|
id | text | text | record_uid | primary, notNull, noUpdate |
created | date | timestamp | record_created | notNull, noInsert, noUpdate, default: CURRENT_TIMESTAMP |
updated | date | timestamp | record_updated | notNull, noInsert, noUpdate, default: CURRENT_TIMESTAMP |
authFields
Nine fields for user authentication. Add these alongside baseFields on your users table.
| Field | type | sqlType | usage | Constraints |
|---|---|---|---|---|
username | text | text | auth_username | notNull, unique |
email | text | text | auth_email | notNull, unique, noUpdate |
email_verified | bool | boolean | auth_email_verified | notNull, noInsert, noUpdate, default: false |
password | text | text | auth_password | notNull, noSelect (hidden from API) |
password_salt | text | text | auth_password_salt | notNull, noSelect, noInsert, noUpdate |
name | text | text | auth_name | notNull |
avatar | file | text | auth_avatar | — |
role | text | text | auth_audience | — |
meta | json | json | auth_metadata | — |
createdTrigger / updatedTrigger
SQL triggers for timestamp management. These are optional — they protect against raw SQL bypassing the API's built-in handling.
{
triggers: [createdTrigger, updatedTrigger],
}| Trigger | Fires | Does |
|---|---|---|
createdTrigger | BEFORE UPDATE of created | Raises an error if the created column is changed. Prevents accidental overwrites. |
updatedTrigger | AFTER UPDATE | Sets updated to CURRENT_TIMESTAMP when a row changes (only if updated wasn't already changed in the same operation). |
Indexes
Indexes improve query performance for frequently filtered or sorted columns.
{
name: 'posts',
fields: [/* ... */],
indexes: [
{ fields: 'email', unique: true }, // Single column, unique
{ fields: ['status', 'created'] }, // Composite index
{ fields: 'category', where: { q: "status = 'active'" } }, // Partial index
],
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | Auto-generated | Index name. Generated from table and column names if omitted. |
unique | boolean | No | false | Create a UNIQUE index. |
fields | string | string[] | Yes | — | Column(s) to index. String for single column, array for composite. May include collation (e.g., 'name COLLATE NOCASE'). |
where | SQLQuery | No | — | Partial index condition. Only index rows matching this WHERE clause. Format: { q: "sql expression" }. |
Triggers
Custom SQL triggers that fire on table events. Use for data integrity rules, computed columns, or audit trails.
{
triggers: [
createdTrigger, // From scaffolds
updatedTrigger,
{
name: 'set_deleted_at',
seq: 'BEFORE',
event: 'UPDATE',
updateOf: 'deleted_by',
body: sql`UPDATE files SET deleted_at = CURRENT_TIMESTAMP
WHERE id = NEW.id
AND OLD.deleted_by IS NULL
AND NEW.deleted_by IS NOT NULL`,
},
],
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Trigger name. Must be unique per table. |
seq | string | No | — | Timing: 'BEFORE', 'AFTER', or 'INSTEAD OF'. |
event | string | Yes | — | Event: 'INSERT', 'UPDATE', or 'DELETE'. |
updateOf | string | string[] | No | — | Only fire on UPDATE of specific column(s). Only valid when event is 'UPDATE'. |
forEach | 'ROW' | No | — | Fire once per affected row (SQLite default behavior). |
body | SQLQuery | SQLQuery[] | Yes | — | SQL to execute. Use sql\...`tagged template. AccessOLD.andNEW.` for row values. |
when | SQLQuery | No | — | Additional condition. Trigger only fires when this evaluates to true. |
Full-Text Search (FTS)
Built on SQLite FTS5. Enables fast text search across specified columns.
{
name: 'posts',
fields: [/* ... */],
fullTextSearch: {
fields: ['title', 'body'],
tokenize: 'porter',
contentless: true,
},
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | boolean | No | true | Enable/disable FTS for this table. |
fields | string[] | Yes | — | Columns to include in the FTS index. Must exist in the table's fields. |
tokenize | string | No | — | FTS5 tokenizer. Options: 'unicode61' (default), 'ascii', 'porter' (stemming), 'trigram' (substring matching). |
prefix | string | No | — | Prefix index sizes for faster prefix queries. E.g., '2,3' indexes 2 and 3 character prefixes. |
contentless | boolean | No | true | Use content table (the actual table) as backing store rather than duplicating data. Saves storage. |
content_rowid | string | No | — | Column to use as rowid for the content table. Must be integer type, cannot have a foreign key. |
columnsize | 0 | 1 | No | — | Store per-column size info. 1 to store (needed for BM25 ranking), 0 to save space. |
detail | string | No | 'full' | Detail level: 'full' (all positions), 'column' (column-level only), 'none' (minimal). Less detail = smaller index. |
Email Configuration
Required for password reset and email verification flows.
{
email: {
from: 'noreply@example.com',
variables: {
company_name: 'My App',
company_url: 'https://myapp.com',
company_address: '123 Main St',
company_copyright: '© 2025 My App',
support_email: 'support@myapp.com',
},
mock: false,
// Choose one provider:
mailgun: {
MAILGUN_API_KEY: '$MAILGUN_API_KEY',
MAILGUN_API_SERVER: 'api.eu.mailgun.net',
},
// — or —
resend: {
RESEND_API_KEY: '$RESEND_API_KEY',
},
},
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
from | string | Yes | — | Sender email address. |
variables | object | Yes | — | Template variables available in all emails. See below. |
mock | boolean | No | false | Log emails to console instead of sending. Useful for development. |
tags | string[] | No | — | Email tags for tracking/filtering in your provider. |
mailgun | object | No | — | Mailgun provider config. |
resend | object | No | — | Resend provider config. |
Template Variables
| Variable | Required | Description |
|---|---|---|
company_name | Yes | Your company/app name. |
company_url | Yes | Your website URL. |
company_address | Yes | Physical address (CAN-SPAM compliance). |
company_copyright | Yes | Copyright line. |
support_email | Yes | Support email for recipients. |
You can add custom key-value pairs — they'll be available in email templates.
Mailgun Options
| Property | Required | Description |
|---|---|---|
MAILGUN_API_KEY | Yes | API key. Prefix with $ for env var. |
MAILGUN_API_SERVER | Yes | API server (e.g., 'api.eu.mailgun.net' for EU). |
MAILGUN_API_URL | No | Custom API URL override. |
MAILGUN_WEBHOOK_ID | No | Webhook ID for delivery tracking. |
MAILGUN_WEBHOOK_SIGNING_KEY | No | Webhook signature verification key. |
EMAIL_BLOCKLIST | No | Comma-separated list of blocked email domains. |
Resend Options
| Property | Required | Description |
|---|---|---|
RESEND_API_KEY | Yes | API key. Prefix with $ for env var. |
RESEND_API_URL | No | Custom API URL override. |
RESEND_WEBHOOK_SECRET | No | Webhook signature secret. |
RESEND_WEBHOOK_ID | No | Webhook ID. |
EMAIL_BLOCKLIST | No | Comma-separated list of blocked email domains. |
Actions
Server-side logic callable via POST /api/v1/action/{name}. See the Actions Guide for full documentation with examples.
{
actions: [
{
name: 'increment_counter',
params: { amount: 'number' },
requireAuth: true,
sql: {
type: 'UPDATE',
table: 'counters',
set: { value: 'value + {:amount}' },
where: sql`id = 1`,
returning: ['*'],
},
},
],
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Action name. Used in API route: /api/v1/action/{name}. |
description | string | No | — | Description, shown in OpenAPI docs. Max 1000 characters. |
params | Record<string, ParamDef> | No | — | Typed parameters. See Actions Guide -- Parameters. |
guard | string | No | — | Expression evaluated before execution. Fails with 403 if false. |
requireAuth | boolean | No | false | Require authentication. Returns 401 if no valid token. |
applyTableRules | boolean | No | true | Apply table-level RLS rules in steps mode. |
sql | object | object[] | No* | — | Raw SQL query objects. *Must have either sql or steps. |
steps | object | object[] | No* | — | Expression-based statements. *Must have either sql or steps. |
Auth Cookie
When you configure authCookie, teenybase will:
- Read the auth token from this cookie in
initAuth()(on every request, after checking theAuthorizationheader) - Set the cookie automatically on OAuth redirect flows (Google One Tap, OAuth callback)
- Delete the cookie on logout
JSON endpoints (login-password, sign-up, refresh-token) return the token in the response body but do not set a cookie. This is by design — setting Set-Cookie on JSON responses breaks CORS with wildcard origins, prevents CDN caching, and causes issues with mobile clients. For SSR apps, set the cookie yourself after calling the auth API (see Connecting Your Frontend — Authentication Flow).
{
authCookie: {
name: 'auth_token',
httpOnly: true, // default: true — prevents JavaScript access
secure: true, // default: true — HTTPS only (set false for local dev)
sameSite: 'Lax', // default: 'Lax'
path: '/', // default: '/'
maxAge: 604800, // optional — cookie lifetime in seconds
domain: '.example.com', // optional — defaults to current domain
},
}| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Cookie name. |
httpOnly | boolean | No | true | Prevent JavaScript access. |
secure | boolean | No | true | Only send over HTTPS. |
sameSite | string | No | 'Lax' | 'Strict', 'Lax', or 'None'. |
path | string | No | '/' | Cookie path. |
maxAge | number | No | — | Cookie lifetime in seconds (session cookie if not set). |
domain | string | No | — | Cookie domain. |
Auth Providers
Configure external authentication providers — OAuth redirect flows, JWT/Bearer token verification, or both. The unified authProviders array replaces the old separate oauthProviders and jwtAllowedIssuers arrays.
How it works:
clientSecretpresent — OAuth redirect flow enabled (authorization code exchange)- Known JWKS provider (e.g.,
'google','supabase') or explicitjwksUrl/secret— Bearer token login enabled (via/auth/login-token) - Both can be active on the same provider (e.g., Google supports OAuth redirect AND One Tap bearer tokens)
{
authProviders: [
// Google (both OAuth redirect + One Tap bearer)
{ name: 'google', clientId: '$GOOGLE_CLIENT_ID', clientSecret: '$GOOGLE_CLIENT_SECRET' },
// GitHub (OAuth only)
{ name: 'github', clientId: '$GITHUB_CLIENT_ID', clientSecret: '$GITHUB_CLIENT_SECRET' },
// Supabase (JWT only, JWKS auto-detected)
{ name: 'supabase', issuer: 'https://xyz.supabase.co/auth/v1' },
// Custom issuer with HMAC secret
{ issuer: 'https://other.example.com', secret: '$OTHER_SECRET' },
// JWKS URL (Auth0, Clerk, Okta, Keycloak, etc.)
{
issuer: 'https://myapp.auth0.com/',
jwksUrl: 'https://myapp.auth0.com/.well-known/jwks.json',
algorithm: 'RS256',
clientId: '$AUTH0_CLIENT_ID',
},
// JWK public key object (RS256/ES256)
{
issuer: 'https://auth.myservice.com',
secret: { kty: 'RSA', n: '...', e: 'AQAB' },
algorithm: 'RS256',
clientId: 'my-client-id',
},
// Custom OAuth provider (fully manual)
{
name: 'custom-provider',
authorizeUrl: 'https://provider.com/oauth/authorize',
tokenUrl: 'https://provider.com/oauth/token',
userinfoUrl: 'https://provider.com/api/userinfo',
clientId: '$CUSTOM_CLIENT_ID',
clientSecret: '$CUSTOM_CLIENT_SECRET',
scopes: ['openid', 'email'],
mapping: { email: 'email_address', avatar: 'photo_url' },
},
// Partial mode: Bearer tokens set auth.email and auth.verified (for email-based rules)
{
issuer: 'https://internal-service.example.com',
secret: '$INTERNAL_SERVICE_SECRET',
bearerMode: 'partial',
},
// Cross-instance teenybase: full mode passes through user/session fields (uid, cid, sid, meta)
{
issuer: 'https://other-app.example.com',
secret: '$OTHER_APP_JWT_SECRET',
bearerMode: 'full',
},
// Admin mode: same as full + trusts the admin flag (only for fully trusted instances)
{
issuer: 'https://admin-app.example.com',
secret: '$ADMIN_APP_JWT_SECRET',
bearerMode: 'admin',
},
],
}Provider Properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | — | Provider name. Built-in presets: 'google', 'github', 'discord', 'linkedin', 'supabase'. Any other string uses manual configuration. |
issuer | string | No | — | JWT iss claim to trust. Required for JWT/Bearer verification when not using a known preset. |
clientId | string | string[] | No | — | OAuth client ID and/or expected JWT aud/azp claim(s). Prefix with $ for env var. |
clientSecret | string | No | — | OAuth client secret. When present, enables OAuth redirect flow. Always use $ env var in production. |
secret | string | JsonWebKey | No | — | HMAC shared secret, PEM public key string, or JWK public key object for JWT verification. Strings support $ENV_VAR syntax. |
jwksUrl | string | No | — | JWKS endpoint URL. Keys are fetched and cached (10 min TTL), matched by kid header. |
algorithm | string | No | Auto-detected | JWT algorithm. Defaults to 'HS256' for string secrets, 'RS256' for JWK/JWKS. Auto-detected from JWK alg field when present. |
bearerMode | 'login' | 'partial' | 'full' | 'admin' | No | 'login' | Controls Bearer token behavior. 'login' (default): tokens only work via /auth/login-token. 'partial': sets auth.email/verified for email-based rules. 'full': passes user/session fields (id, cid, sid, meta, aud) for cross-instance auth. 'admin': same as full + trusts the admin flag. |
scopes | string[] | No | Provider default | OAuth scopes to request. |
redirectUrl | string | No | — | Frontend URL to redirect after successful OAuth. |
authorizeUrl | string | No | Preset | OAuth authorization endpoint (manual providers only). |
tokenUrl | string | No | Preset | Token exchange endpoint. |
userinfoUrl | string | No | Preset | Userinfo endpoint for fetching profile data. |
userinfoHeaders | Record<string, string> | No | — | Additional headers for userinfo request. |
userinfoField | string | No | — | Extract nested field from userinfo response (e.g., 'user' for Discord). |
authorizeParams | Record<string, string> | No | — | Additional query params for authorization URL. |
mapping | object | No | Defaults | Map provider fields to teenybase user fields. See below. |
Field Mapping
mapping: {
email: 'email', // Default: 'email'
name: 'name', // Default: 'name'
username: 'login', // No default
avatar: 'picture', // No default
verified: 'email_verified', // Default: 'email_verified'
}JWT Verification Key
You can provide the key in two ways:
secret— HMAC shared secret string ($ENV_VARsupported), PEM public key string, or JWK public key objectjwksUrl— JWKS endpoint URL to auto-fetch and cache public keys (standard for Auth0, Clerk, Okta, etc.)
See the OAuth Guide for provider-specific setup instructions.
How JWT Signing Works
Teenybase uses a double-secret approach for table auth tokens. The actual signing key is the global jwtSecret concatenated with the table-level jwtSecret:
signing_key = global_jwtSecret + table_jwtSecretThis means:
- Changing the global secret invalidates all tokens across all tables
- Changing a table's secret only invalidates that table's tokens
- Both secrets must be present for table auth tokens to sign and verify correctly
- Tokens without a table claim (e.g. admin tokens) use only the global secret
// teenybase.ts
export default {
jwtSecret: '$JWT_SECRET', // global — part of every signing key
tables: [{
name: 'users',
extensions: [{
name: 'auth',
jwtSecret: '$JWT_SECRET_USERS', // table — combined with global
// actual signing key = JWT_SECRET + JWT_SECRET_USERS
}],
}],
}Use different $ENV_VAR names for the global and each table secret. This way, rotating one table's secret doesn't affect other tables.
Environment Variable Resolution
Any string value prefixed with $ is resolved from environment variables at runtime. This keeps secrets out of your config file.
jwtSecret: '$JWT_SECRET', // Resolves to the value of JWT_SECRET env var- Local dev: Read from
.dev.varsfile in your project root - Production (self-hosted): Read from Cloudflare Worker secrets (upload via
teeny secrets --remote --uploadfrom.prod.vars) - Production (managed): Read from platform secrets (upload via
teeny secrets --remote --uploadfrom.prod.vars)
Example .dev.vars:
JWT_SECRET=my-local-dev-secret-change-in-production
GOOGLE_CLIENT_ID=1234567890.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxx
MAILGUN_API_KEY=key-xxxxxxxxObservability (wrangler.jsonc)
The observability block in wrangler.jsonc enables Cloudflare Workers Logs — stored for 7 days on paid plans, 3 days on free. Generated by teeny init with sensible defaults. This is a wrangler.jsonc setting, not a teenybase.ts setting — it controls Cloudflare platform-level logging, not application-level config.
"observability": {
"enabled": true,
"logs": {
"invocation_logs": true,
"head_sampling_rate": 1
}
}| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master switch for Workers Logs |
logs.invocation_logs | boolean | true | Log automatic invocation data (request/response metadata) |
logs.head_sampling_rate | number | 1 | Fraction of requests to log (1 = 100%, 0.01 = 1%) |
Stream logs via teeny logs -- see CLI Reference.