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...typesqlTypeExample
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)

typescript
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
PropertyTypeRequiredDefaultDescription
appUrlstringYesYour app's URL. Used for OAuth redirects, email links, and CORS.
jwtSecretstringYesGlobal JWT signing secret. Combined with table-level jwtSecret (concatenated) for table auth tokens. Prefix with $ to resolve from env vars (e.g., '$JWT_SECRET').
tablesTableData[]YesArray of table definitions. Each becomes a SQLite table with REST endpoints.
appNamestringNo'Teeny App'Application name, used in email templates as the APP_NAME variable.
jwtIssuerstringNo'$db'JWT iss claim value for tokens created by this instance.
jwtAlgorithmstringNo'HS256'JWT signing algorithm.
authProvidersAuthProvider[]No[]External auth provider configurations — OAuth redirect flows, JWT verification, or both. See Auth Providers.
allowedRedirectUrlsstring[]NoAllowed redirect URLs after OAuth login (exact match). When not set, URLs matching appUrl hostname are allowed. See OAuth Guide.
authCookieAuthCookieConfigNoCookie-based auth for SSR and OAuth flows. See Auth Cookie.
emailEmailSettingsNoEmail provider config for verification/reset emails. See Email Configuration.
actionsSQLAction[]No[]Server-side actions callable via API. See Actions Guide.
versionnumberNoSchema version number, tracked across migrations.

Table Definition (TableData)

typescript
{
    name: 'posts',
    fields: [/* ... */],

    // Optional
    autoSetUid: true,
    extensions: [/* ... */],
    triggers: [/* ... */],
    indexes: [/* ... */],
    fullTextSearch: {/* ... */},
    r2Base: 'posts',
    idInR2: false,
    autoDeleteR2Files: true,
    allowMultipleFileRef: false,
    allowWildcard: false,
}
PropertyTypeRequiredDefaultDescription
namestringYesTable name. Used in API routes: /api/v1/table/{name}/. Must be a valid SQL identifier.
fieldsTableFieldData[]YesColumn definitions. See Field Definition.
autoSetUidbooleanNofalseAuto-generate a unique ID for the record_uid field on insert. Requires a field with usage: 'record_uid' and type: 'text'.
extensionsTableExtensionData[]No[]Extensions to enable: auth, rules, crud. CRUD is auto-included.
triggersSQLTrigger[]No[]SQL triggers. See Triggers.
indexesSQLIndex[]No[]Database indexes. See Indexes.
fullTextSearchobjectNoFull-text search config. See Full-Text Search.
r2BasestringNoR2 bucket path prefix for file storage. Required if any field has type: 'file'.
idInR2booleanNofalseStore files under {r2Base}/{recordId}/ subdirectories in R2. Cannot be used with allowMultipleFileRef. Requires a record_uid field with noUpdate: true.
autoDeleteR2FilesbooleanNotrueAutomatically delete R2 files when records are deleted.
allowMultipleFileRefbooleanNofalseAllow referencing the same file from multiple records. Cannot be used with idInR2. Requires autoDeleteR2Files: false.
allowWildcardbooleanNofalseAllow * wildcard in select queries.

Field Definition (TableFieldData)

typescript
{
    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,
}
PropertyTypeRequiredDefaultDescription
namestringYesColumn name. Must start with a letter or underscore; alphanumeric + underscores only.
typestringYesTeenybase field type. Controls API-level behavior. See Field Types.
sqlTypestringYesSQLite column type. Controls storage. See SQL Types.
primarybooleanNofalsePRIMARY KEY constraint.
autoIncrementbooleanNofalseAUTOINCREMENT (integer primary keys only).
uniquebooleanNofalseUNIQUE constraint.
notNullbooleanNofalseNOT NULL constraint.
defaultSQLLiteral | stringNoDefault value. Use sqlValue('text') for literal values or sql\CURRENT_TIMESTAMP`` for SQL expressions.
checkSQLLiteral | stringNoCHECK constraint. Example: sql`json_valid(config)`.
collatestringNoCollation. 'BINARY', 'NOCASE', or 'RTRIM'.
foreignKeyFieldForeignKeyNoForeign key relation. See below.
usagestringNoSemantic usage that extensions act on. See Field Usages.
noSelectbooleanNofalseExclude from API responses (hidden field). Used for passwords, salts, etc.
noInsertbooleanNofalseCannot be set via API insert. Used for auto-generated fields.
noUpdatebooleanNofalseCannot be changed via API update. Used for IDs, creation timestamps.

Foreign Key Options

typescript
foreignKey: {
    table: 'users',       // Referenced table name
    column: 'id',         // Referenced column name
    onDelete: 'CASCADE',  // Action on parent delete
    onUpdate: 'CASCADE',  // Action on parent update
}
ActionDescription
'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.

TypeDescriptionCompatible sqlTypes
textPlain text stringtext, null
numberFloating-point numberinteger, real
integerInteger numberinteger
boolBoolean (true/false)boolean, integer
emailEmail address (validated format)text
urlURL stringtext
editorRich text / HTML contenttext
dateDate or datetime valuetext, timestamp, datetime, date, time
selectEnum / dropdown valuetext, integer, real
jsonJSON object or arraytext, json
fileFile reference (stored in R2)text
relationForeign key referencetext, integer, real
passwordPassword field (hashed)text
blobBinary datablob

SQL Types

The sqlType property maps to the actual SQLite column type used in DDL.

sqlTypeSQLite AffinityNotes
textTEXTMost common. Strings, JSON, file paths, relations.
integerINTEGERIntegers, booleans (0/1).
realREALFloating-point numbers.
booleanNUMERICStored as 0/1 in SQLite.
blobBLOBBinary data.
jsonTEXTStored as JSON string. Can use json_valid() CHECK constraint.
timestampTEXTISO 8601 datetime string. Used with CURRENT_TIMESTAMP.
datetimeTEXTAlias for timestamp.
dateTEXTDate-only string.
timeTEXTTime-only string.
floatREALAlias for real.
intINTEGERAlias for integer.
numericNUMERICSQLite numeric affinity.
nullNULL 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

UsageDescriptionAuto behavior
record_uidUnique record identifierAuto-generated on insert (when autoSetUid: true). Used as the record's primary identifier in API responses.
record_createdCreation timestampSet to CURRENT_TIMESTAMP on insert. Protected from updates via trigger.
record_updatedLast update timestampSet to CURRENT_TIMESTAMP on every update via trigger.

Auth Usages

These are recognized by the Auth Extension and enable automatic authentication behavior.

UsageDescriptionAuto behavior
auth_emailUser's email addressUsed as login identity. Must be unique.
auth_usernameUsernameUsed as alternative login identity. Must be unique.
auth_passwordPassword hashAutomatically hashed on sign-up. Hidden from API responses (noSelect).
auth_password_saltPassword saltAuto-generated. Hidden from API responses.
auth_email_verifiedEmail verification statusSet to false on sign-up. Updated on verification confirm.
auth_nameDisplay nameIncluded in JWT payload.
auth_avatarProfile picture (file)Stored in R2 via file upload.
auth_audienceRole / audienceIncluded in JWT aud claim. Used for role-based access.
auth_metadataUser 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.

typescript
{
    name: 'auth',
    jwtSecret: '$JWT_SECRET_USERS',   // required
    jwtTokenDuration: 3600,           // required (seconds)
    maxTokenRefresh: 5,               // required (0 = unlimited)
} as TableAuthExtensionData

JWT (required):

PropertyTypeDefaultDescription
jwtSecretstringRequiredTable-level JWT secret. Concatenated with global jwtSecret to form the signing key. Use $ prefix for env vars.
jwtTokenDurationnumberRequiredJWT access token expiry in seconds.
maxTokenRefreshnumberRequiredMax refreshes before re-login. Set to 0 for unlimited.

Passwords:

PropertyTypeDefaultDescription
passwordType'sha256''sha256'Password hashing algorithm. Currently only sha256 supported.
passwordConfirmSuffixstringSuffix for password confirmation field (e.g., 'Confirm'passwordConfirm). When set, sign-up requires both fields to match.
passwordCurrentSuffixstring'Current'Suffix for current password field on update (e.g., passwordCurrent). Set to '' to disable (not recommended).

Email and verification:

PropertyTypeDefaultDescription
autoSendVerificationEmailbooleanfalseSend verification email on sign-up. Requires email service config.
normalizeEmailbooleantrueLowercase, trim, punycode domains, provider-specific rules (gmail: remove dots/plus-addressing).
passwordResetTokenDurationnumber3600Reset token validity in seconds.
emailVerifyTokenDurationnumber3600Verification token validity in seconds.
passwordResetEmailDurationnumber120Min interval between reset emails (seconds).
emailVerifyEmailDurationnumber120Min interval between verification emails (seconds).
emailTemplatesobjectCustom templates for verification and passwordReset. See below.

OAuth:

PropertyTypeDefaultDescription
saveIdentitiesbooleanfalseSave OAuth provider identity data in _auth_identities table.

Email Templates

typescript
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
    },
}
PropertyTypeDescription
subjectstringEmail subject line. Supports placeholders.
layoutHtmlstring | 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 .
variablesRecord<string, any>Additional template variables merged into the email.
tagsstringTags 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):

ClaimSource field usageDescription
idrecord_uidUser's unique record ID
subauth_emailUser's email address
userauth_usernameUser's username (note: claim is user, not username)
audauth_audienceUser's role(s) — only included if non-empty
verifiedauth_email_verifiedWhether email is verified — only included if field exists
metaauth_metadataCustom metadata (JSON) — only included if field exists
cidTable name the token was issued for (e.g. "users")
sidSession ID (for refresh token validation)
iatIssued at (Unix timestamp)
expExpires at (Unix timestamp)
issIssuer (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.

typescript
{
    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',
}
PropertyTypeDefaultDescription
listRulestring | nullnullFilter for list/select queries. Converted to SQL WHERE clause. null blocks all access (403).
viewRulestring | nullnullFilter for single-record view. Converted to SQL WHERE clause. null blocks all access (403).
createRulestring | nullnullGuard for insert operations. Evaluated against new record values at parse time. null blocks all access (403).
updateRulestring | nullnullFilter for update operations. Converted to SQL WHERE clause. null blocks all access (403).
deleteRulestring | nullnullFilter for delete operations. Converted to SQL WHERE clause. null blocks all access (403).

Rule Values

ValueBehavior
'true'Allow all requests, including unauthenticated.
'false'Deny all requests.
null or omittedDeny all requests (no rule = no access).
Expression stringEvaluated 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:

VariableDescription
auth.uidAuthenticated user's ID. null if not logged in.
auth.emailUser's email from JWT.
auth.roleUser's role/audience from JWT aud claim.
auth.verifiedWhether user's email is verified (boolean).
auth.adminWhether user is admin (boolean).
auth.superadminWhether user is superadmin (boolean).
auth.metaUser metadata from JWT meta claim.
auth.jwtRaw JWT payload (all claims).
Column namesAny 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:

OperatorSQL EquivalentExample
==ISauth.uid == id
!=IS NOTauth.uid != null
==role = 'guest'
>>price > 0
<<age < 18
>=>=priority >= 5
<=<=count <= 100
~LIKEemail ~ '%@example.com'
!~NOT LIKErole !~ '%admin'
!NOT (unary)!deleted_at
&ANDauth.uid == id & published == true
|ORrole == 'admin' | role == 'editor'
|||| (concat)path ~ (auth.meta.base || '%')
@@MATCH (FTS5)articles @@ 'search term'

SQL functions available in expressions:

FunctionDescriptionExample
lower(x)Lowercaselower(email) == 'admin@test.com'
upper(x)Uppercaseupper(status) == 'ACTIVE'
length(x)String lengthlength(name) > 3
substring(x, start, len)Substringsubstring(code, 1, 2) == 'US'
replace(x, from, to)Replacereplace(name, ' ', '-')
concat(a, b, ...)Concatenateconcat(first, ' ', last)
count(x), sum(x)AggregatesSELECT/actions only
datetime(x), date(x), time(x)Date/timedate(created) == date('now')
unixepoch(x)Unix timestampunixepoch('now') - unixepoch(created) < 3600
json_set, json_insert, json_replace, json_patchJSON mutationjson_set(meta, '$.key', 'value')
json_contains(json, val)JSON array membershipjson_contains(tags, 'important')

Rule Examples

typescript
// 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.

typescript
{ name: 'crud' }

No additional configuration. The CRUD extension registers these routes:

MethodPathDescription
GET/POST/selectQuery records
GET/POST/listQuery records with total count
GET/view/:idGet single record by ID
POST/insertInsert one or more records
POST/updateUpdate records by filter
POST/edit/:idUpdate a single record by ID
POST/deleteDelete 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.

typescript
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
ParameterTypeDefaultDescription
db$Database(required)The database instance
swaggerbooleantrueEnable Swagger UI at /doc/ui

Routes (under /api/v1):

MethodPathDescription
GET/api/v1/docOpenAPI 3.1.0 JSON spec
GET/api/v1/doc/uiSwagger 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 authorization header 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:

typescript
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':

typescript
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

FieldTypeDefaultDescription
mode'managed' | 'direct'managed for Teenybase Cloud, direct for self-hosted
workerNamestringauto-derived in managed modeWorker script name for log queries. Required in direct mode.
baseUrlstringCDN (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)
uiVersionstring'latest'Version of @teenybase/pocket-ui npm package (only used with CDN baseUrl)
platformUrlstringPLATFORM_BASE_URL envManaged mode only. Teenybase platform origin for usage/logs API calls.
usageResourcesobjectUSAGE_RESOURCES env (JSON)Direct mode only. Declares which CF resources to query for the usage dashboard. See below.

usageResources shape:

typescript
{
    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:

MethodPathDescription
GET/pocket/Admin panel UI (proxied from CDN or custom baseUrl)
GET/pocket/loginLogin page
POST/pocket/loginAuthenticate with username + password
GET/pocket/logoutClear session cookies, redirect to login

Storage browser (requires R2 bucket binding):

MethodPathDescription
GET/storage/listList objects in R2 bucket (supports prefix, cursor, limit, delimiter query params)
GET/storage/object/:keyDownload an object
PUT/storage/object/:keyUpload an object (editor+ role required)
DELETE/storage/object/:keyDelete an object (editor+ role required)

Usage dashboard:

MethodPathDescription
GET/pocket/usage?range=24hUsage data with cost breakdown. Ranges: 1h, 6h, 24h, 7d, 30d.

Log viewer (only available when workerName is resolved):

MethodPathDescription
GET/logs/workersList workers available for log streaming
POST/logs/queryQuery worker telemetry logs

Environment variables

VariableModeDescription
POCKET_UI_VIEWER_PASSWORDbothPassword for viewer role (read-only)
POCKET_UI_EDITOR_PASSWORDbothPassword for editor role (read + write)
ADMIN_SERVICE_TOKENbothAlso accepted as superadmin password
CF_API_TOKENdirectCloudflare API token with Workers/D1/R2 read permissions
CF_ACCOUNT_IDdirectYour Cloudflare account ID
USAGE_RESOURCESdirectJSON alternative to usageResources config field
PLATFORM_WORKER_TOKENmanagedAuto-set by Teenybase Cloud
PLATFORM_BASE_URLmanagedAuto-set by Teenybase Cloud
  • 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, Secure only on HTTPS

Scaffolds

Pre-built field and trigger definitions importable from teenybase/scaffolds/fields. These save boilerplate for common table patterns.

typescript
import { baseFields, authFields, createdTrigger, updatedTrigger } from 'teenybase/scaffolds/fields'

baseFields

Three fields every table should have: a unique ID, creation timestamp, and update timestamp.

FieldtypesqlTypeusageConstraints
idtexttextrecord_uidprimary, notNull, noUpdate
createddatetimestamprecord_creatednotNull, noInsert, noUpdate, default: CURRENT_TIMESTAMP
updateddatetimestamprecord_updatednotNull, noInsert, noUpdate, default: CURRENT_TIMESTAMP

authFields

Nine fields for user authentication. Add these alongside baseFields on your users table.

FieldtypesqlTypeusageConstraints
usernametexttextauth_usernamenotNull, unique
emailtexttextauth_emailnotNull, unique, noUpdate
email_verifiedboolbooleanauth_email_verifiednotNull, noInsert, noUpdate, default: false
passwordtexttextauth_passwordnotNull, noSelect (hidden from API)
password_salttexttextauth_password_saltnotNull, noSelect, noInsert, noUpdate
nametexttextauth_namenotNull
avatarfiletextauth_avatar
roletexttextauth_audience
metajsonjsonauth_metadata

createdTrigger / updatedTrigger

SQL triggers for timestamp management. These are optional — they protect against raw SQL bypassing the API's built-in handling.

typescript
{
    triggers: [createdTrigger, updatedTrigger],
}
TriggerFiresDoes
createdTriggerBEFORE UPDATE of createdRaises an error if the created column is changed. Prevents accidental overwrites.
updatedTriggerAFTER UPDATESets 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.

typescript
{
    name: 'posts',
    fields: [/* ... */],
    indexes: [
        { fields: 'email', unique: true },           // Single column, unique
        { fields: ['status', 'created'] },            // Composite index
        { fields: 'category', where: { q: "status = 'active'" } },  // Partial index
    ],
}
PropertyTypeRequiredDefaultDescription
namestringNoAuto-generatedIndex name. Generated from table and column names if omitted.
uniquebooleanNofalseCreate a UNIQUE index.
fieldsstring | string[]YesColumn(s) to index. String for single column, array for composite. May include collation (e.g., 'name COLLATE NOCASE').
whereSQLQueryNoPartial 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.

typescript
{
    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`,
        },
    ],
}
PropertyTypeRequiredDefaultDescription
namestringYesTrigger name. Must be unique per table.
seqstringNoTiming: 'BEFORE', 'AFTER', or 'INSTEAD OF'.
eventstringYesEvent: 'INSERT', 'UPDATE', or 'DELETE'.
updateOfstring | string[]NoOnly fire on UPDATE of specific column(s). Only valid when event is 'UPDATE'.
forEach'ROW'NoFire once per affected row (SQLite default behavior).
bodySQLQuery | SQLQuery[]YesSQL to execute. Use sql\...`tagged template. AccessOLD.andNEW.` for row values.
whenSQLQueryNoAdditional condition. Trigger only fires when this evaluates to true.

Full-Text Search (FTS)

Built on SQLite FTS5. Enables fast text search across specified columns.

typescript
{
    name: 'posts',
    fields: [/* ... */],
    fullTextSearch: {
        fields: ['title', 'body'],
        tokenize: 'porter',
        contentless: true,
    },
}
PropertyTypeRequiredDefaultDescription
enabledbooleanNotrueEnable/disable FTS for this table.
fieldsstring[]YesColumns to include in the FTS index. Must exist in the table's fields.
tokenizestringNoFTS5 tokenizer. Options: 'unicode61' (default), 'ascii', 'porter' (stemming), 'trigram' (substring matching).
prefixstringNoPrefix index sizes for faster prefix queries. E.g., '2,3' indexes 2 and 3 character prefixes.
contentlessbooleanNotrueUse content table (the actual table) as backing store rather than duplicating data. Saves storage.
content_rowidstringNoColumn to use as rowid for the content table. Must be integer type, cannot have a foreign key.
columnsize0 | 1NoStore per-column size info. 1 to store (needed for BM25 ranking), 0 to save space.
detailstringNo'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.

typescript
{
    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',
        },
    },
}
PropertyTypeRequiredDefaultDescription
fromstringYesSender email address.
variablesobjectYesTemplate variables available in all emails. See below.
mockbooleanNofalseLog emails to console instead of sending. Useful for development.
tagsstring[]NoEmail tags for tracking/filtering in your provider.
mailgunobjectNoMailgun provider config.
resendobjectNoResend provider config.

Template Variables

VariableRequiredDescription
company_nameYesYour company/app name.
company_urlYesYour website URL.
company_addressYesPhysical address (CAN-SPAM compliance).
company_copyrightYesCopyright line.
support_emailYesSupport email for recipients.

You can add custom key-value pairs — they'll be available in email templates.

Mailgun Options

PropertyRequiredDescription
MAILGUN_API_KEYYesAPI key. Prefix with $ for env var.
MAILGUN_API_SERVERYesAPI server (e.g., 'api.eu.mailgun.net' for EU).
MAILGUN_API_URLNoCustom API URL override.
MAILGUN_WEBHOOK_IDNoWebhook ID for delivery tracking.
MAILGUN_WEBHOOK_SIGNING_KEYNoWebhook signature verification key.
EMAIL_BLOCKLISTNoComma-separated list of blocked email domains.

Resend Options

PropertyRequiredDescription
RESEND_API_KEYYesAPI key. Prefix with $ for env var.
RESEND_API_URLNoCustom API URL override.
RESEND_WEBHOOK_SECRETNoWebhook signature secret.
RESEND_WEBHOOK_IDNoWebhook ID.
EMAIL_BLOCKLISTNoComma-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.

typescript
{
    actions: [
        {
            name: 'increment_counter',
            params: { amount: 'number' },
            requireAuth: true,
            sql: {
                type: 'UPDATE',
                table: 'counters',
                set: { value: 'value + {:amount}' },
                where: sql`id = 1`,
                returning: ['*'],
            },
        },
    ],
}
PropertyTypeRequiredDefaultDescription
namestringYesAction name. Used in API route: /api/v1/action/{name}.
descriptionstringNoDescription, shown in OpenAPI docs. Max 1000 characters.
paramsRecord<string, ParamDef>NoTyped parameters. See Actions Guide -- Parameters.
guardstringNoExpression evaluated before execution. Fails with 403 if false.
requireAuthbooleanNofalseRequire authentication. Returns 401 if no valid token.
applyTableRulesbooleanNotrueApply table-level RLS rules in steps mode.
sqlobject | object[]No*Raw SQL query objects. *Must have either sql or steps.
stepsobject | object[]No*Expression-based statements. *Must have either sql or steps.

When you configure authCookie, teenybase will:

  • Read the auth token from this cookie in initAuth() (on every request, after checking the Authorization header)
  • 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).

typescript
{
    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
    },
}
PropertyTypeRequiredDefaultDescription
namestringYesCookie name.
httpOnlybooleanNotruePrevent JavaScript access.
securebooleanNotrueOnly send over HTTPS.
sameSitestringNo'Lax''Strict', 'Lax', or 'None'.
pathstringNo'/'Cookie path.
maxAgenumberNoCookie lifetime in seconds (session cookie if not set).
domainstringNoCookie 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:

  • clientSecret present — OAuth redirect flow enabled (authorization code exchange)
  • Known JWKS provider (e.g., 'google', 'supabase') or explicit jwksUrl/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)
typescript
{
    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

PropertyTypeRequiredDefaultDescription
namestringNoProvider name. Built-in presets: 'google', 'github', 'discord', 'linkedin', 'supabase'. Any other string uses manual configuration.
issuerstringNoJWT iss claim to trust. Required for JWT/Bearer verification when not using a known preset.
clientIdstring | string[]NoOAuth client ID and/or expected JWT aud/azp claim(s). Prefix with $ for env var.
clientSecretstringNoOAuth client secret. When present, enables OAuth redirect flow. Always use $ env var in production.
secretstring | JsonWebKeyNoHMAC shared secret, PEM public key string, or JWK public key object for JWT verification. Strings support $ENV_VAR syntax.
jwksUrlstringNoJWKS endpoint URL. Keys are fetched and cached (10 min TTL), matched by kid header.
algorithmstringNoAuto-detectedJWT 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.
scopesstring[]NoProvider defaultOAuth scopes to request.
redirectUrlstringNoFrontend URL to redirect after successful OAuth.
authorizeUrlstringNoPresetOAuth authorization endpoint (manual providers only).
tokenUrlstringNoPresetToken exchange endpoint.
userinfoUrlstringNoPresetUserinfo endpoint for fetching profile data.
userinfoHeadersRecord<string, string>NoAdditional headers for userinfo request.
userinfoFieldstringNoExtract nested field from userinfo response (e.g., 'user' for Discord).
authorizeParamsRecord<string, string>NoAdditional query params for authorization URL.
mappingobjectNoDefaultsMap provider fields to teenybase user fields. See below.

Field Mapping

typescript
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_VAR supported), PEM public key string, or JWK public key object
  • jwksUrl — 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_jwtSecret

This 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
typescript
// 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.

typescript
jwtSecret: '$JWT_SECRET',    // Resolves to the value of JWT_SECRET env var
  • Local dev: Read from .dev.vars file in your project root
  • Production (self-hosted): Read from Cloudflare Worker secrets (upload via teeny secrets --remote --upload from .prod.vars)
  • Production (managed): Read from platform secrets (upload via teeny secrets --remote --upload from .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-xxxxxxxx

Observability (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.

jsonc
"observability": {
    "enabled": true,
    "logs": {
        "invocation_logs": true,
        "head_sampling_rate": 1
    }
}
FieldTypeDefaultDescription
enabledbooleantrueMaster switch for Workers Logs
logs.invocation_logsbooleantrueLog automatic invocation data (request/response metadata)
logs.head_sampling_ratenumber1Fraction of requests to log (1 = 100%, 0.01 = 1%)

Stream logs via teeny logs -- see CLI Reference.