Runtime & ctx
Every function receives two arguments: input (the JSON payload) and ctx (the runtime context). The ctx object is the primary way functions interact with your database.
module.exports = { handler: async (input, ctx) => { // ctx.db — SQL query interface // ctx.fs9 — filesystem read/write interface // ctx.self — metadata about the current function run }};ctx.self
Section titled “ctx.self”Metadata about the current execution:
| Property | Type | Description |
|---|---|---|
functionId | string | UUID of the function |
versionId | string | UUID of the active version being executed |
runId | string | UUID of the current run |
triggerType | string | How the function was invoked: invoke, cron, or api |
module.exports = { handler: async (input, ctx) => { console.log(`Run ${ctx.self.runId} triggered via ${ctx.self.triggerType}`); return { runId: ctx.self.runId }; }};ctx.db
Section titled “ctx.db”SQL query interface. Has a single method:
ctx.db.query(sql, params?)
Section titled “ctx.db.query(sql, params?)”Execute a SQL statement with optional parameterized values.
const result = await ctx.db.query( "SELECT id, name, email FROM users WHERE active = $1 LIMIT $2", [true, 10]);Parameters:
sql— SQL string with$1,$2, etc. for parameter placeholdersparams— optional array of parameter values
Returns a QueryResult:
| Field | Type | Description |
|---|---|---|
columns | Array<{name, type}> | Column metadata |
rows | unknown[][] | Row tuples, ordered to match columns |
row_count | number | Number of rows affected or returned |
command | string | SQL command tag: SELECT, INSERT, UPDATE, DELETE, etc. |
Rows are returned as arrays of values (not objects). Map them yourself if you need named fields:
module.exports = { handler: async (input, ctx) => { const result = await ctx.db.query("SELECT id, name, email FROM users"); const users = result.rows.map(([id, name, email]) => ({ id, name, email })); return { users, total: result.row_count }; }};SQL Access Patterns
Section titled “SQL Access Patterns”Read data
Section titled “Read data”module.exports = { handler: async (input, ctx) => { const result = await ctx.db.query( "SELECT id, title, status FROM tasks WHERE assignee = $1 ORDER BY created_at DESC", [input.userId] ); return { tasks: result.rows.map(([id, title, status]) => ({ id, title, status })), count: result.row_count }; }};Write data
Section titled “Write data”module.exports = { handler: async (input, ctx) => { await ctx.db.query( "INSERT INTO events (type, payload, created_at) VALUES ($1, $2, NOW())", [input.eventType, JSON.stringify(input.data)] ); return { recorded: true }; }};Transactions
Section titled “Transactions”Aggregations
Section titled “Aggregations”module.exports = { handler: async (input, ctx) => { const result = await ctx.db.query(` SELECT date_trunc('day', created_at) as day, count(*) as total, count(DISTINCT user_id) as unique_users FROM events WHERE created_at >= NOW() - INTERVAL '7 days' GROUP BY 1 ORDER BY 1 DESC `); return { metrics: result.rows.map(([day, total, unique_users]) => ({ day, total, unique_users })) }; }};Permission Setup
Section titled “Permission Setup”Functions run as the authenticated role. Tables created by the admin role need explicit grants:
GRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated;GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO authenticated;ctx.fs9
Section titled “ctx.fs9”Filesystem interface for reading and writing files in your database’s storage. Use --fs9-scope at deploy time to declare which paths your function accesses.
| Method | Description |
|---|---|
read(path) | Read file contents as UTF-8 string |
readBase64(path) | Read file contents as base64-encoded string |
write(path, content) | Write or overwrite a file |
list(path?) | List directory entries with metadata |
stat(path) | Get file metadata (size, type, mtime, generation, mode, sealed, storage) |
delete(path) | Delete a file or directory |
module.exports = { handler: async (input, ctx) => { // Write a file await ctx.fs9.write("/reports/summary.txt", "Report content here");
// Read it back const content = await ctx.fs9.read("/reports/summary.txt");
// List directory contents const entries = await ctx.fs9.list("/reports/");
// Get file metadata const stat = await ctx.fs9.stat("/reports/summary.txt"); // stat: { path, type: "file", size: 19, mtime: "2026-04-01T...", // generation: 2, mode: 420, sealed: false, storage: "inline" }
return { content, fileCount: entries.length, stat }; }};The list() and stat() responses include full metadata for each entry:
| Field | Type | Description |
|---|---|---|
path | string | Full path of the entry |
type | string | file or dir |
size | number | Size in bytes |
mtime | string | Last modified time (ISO 8601) |
generation | number | Version generation counter |
mode | number | Unix file mode |
sealed | boolean | Whether the file is sealed (immutable) |
storage | string | Storage backend (e.g., inline) |
Reading binary files
Section titled “Reading binary files”For binary files (images, xlsx, zip), use readBase64:
const base64 = await ctx.fs9.readBase64("/data/image.png");const buffer = Buffer.from(base64, "base64");Bundling and Dependencies
Section titled “Bundling and Dependencies”The default deployment mode is a single file. If your function needs npm packages or multiple source files, bundle them with esbuild before deploying.
Why bundle?
Section titled “Why bundle?”- The runtime does not have access to
node_modules require()of external packages fails at runtime- Multi-file
import ./utilsis not supported
Bundling with esbuild
Section titled “Bundling with esbuild”1. Initialize a project:
mkdir my-function && cd my-functionnpm init -ynpm install --save-dev esbuild2. Install dependencies you need:
npm install xlsx csv-stringify3. Write your function:
const XLSX = require("xlsx");
module.exports = { handler: async (input, ctx) => { const base64 = await ctx.fs9.readBase64(input.path); const workbook = XLSX.read(Buffer.from(base64, "base64"), { type: "buffer" }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const csv = XLSX.utils.sheet_to_csv(sheet); const outPath = input.path.replace(/\.xlsx?$/, ".csv"); await ctx.fs9.write(outPath, csv); return { output: outPath, rows: csv.split("\n").length }; }};4. Add a build script to package.json:
{ "scripts": { "build": "esbuild src/index.js --bundle --platform=node --target=es2020 --outfile=dist/index.js" }}5. Build and deploy:
npm run buildcat dist/index.js | db9 functions create xlsx-to-csv --db myapp -f - \ --fs9-scope /data:ro --fs9-scope /output:rwBundle size limits
Section titled “Bundle size limits”Keep bundles under 5 MB for reliable deployments. Use esbuild’s tree-shaking and --external flag to minimize size.
Next Steps
Section titled “Next Steps”- Overview — Quick start and deployment basics
- Configuration — Secrets, fs9 scope, network, cron, limits
- Examples — Real-world patterns