Build site backend workflows with Func
A complete agent-oriented Func specification covering files and methods, runtime APIs, data, auth, secrets, third parties, payments, assets, timeouts, native SSE, and SSR boundaries.
Func is the project-scoped backend runtime for Creght / Talizen. Use it for small server-side workflows such as protected writes, bookings and waitlists, profile updates, third-party APIs, webhooks, payment integrations, and AI requests that need secrets or bounded streaming.
When to use Func
Good Func workloads
Bookings, waitlists, RSVP, lead routing, profile updates, availability checks, signed-in actions, third-party APIs, webhooks, payments, and simple JSON data reads/writes.
Prefer built-ins
Use CMS for ordinary content, talizen/form for static contact forms, and talizen/auth for login UI and session state.
Not a Func workload
Detached background jobs, heavy file processing, unbounded streams, timer polling, custom identity/session systems, OAuth callbacks, or token exchange.
Project isolation
Func is already scoped to the current project. Inputs, source code, and branches must not contain project_id, site_id, or internal table IDs.
Files, keys, and methods
Func files live in /backend/func. The extensionless file path is the Func key: /backend/func/booking.ts maps to booking, and /backend/func/profile/settings.ts maps to profile/settings.
Dots are reserved for exported methods. Export main for one operation, or export related operations directly from one file. Do not write a manual dispatcher.
// /backend/func/booking.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'
export function create(input, ctx: TalizenFuncContext) {
if (!input?.startAt) throw new Error('startAt is required')
const user = ctx.auth.requireUser()
return ctx.db.insert('appointments', {
startAt: input.startAt,
userId: user.id,
})
}
export function availability(input, ctx: TalizenFuncContext) {
return ctx.db.query('appointments', { date: input.date })
}
invoke('booking.create', input) // key booking, method create
invoke('profile/settings.update', input)
invoke('booking', input) // key booking, method main
Runtime and code rules
- Use ESM exports:
export function method(input, ctx). Import only TypeScript types fromtalizen/func-runtimewhen needed. - Access every platform capability through
ctx. Do not use legacy globals such asdata,db,auth, orcache. - Validate, trim, and normalize all input inside the Func. Return structured JSON for expected business states; throw for invalid requests or unexpected failures.
- Func is not a full Node.js runtime. Do not depend on Node built-ins.
setTimeout/setIntervalare unsupported and must not be used for delays, polling, or retries. - Standard
fetch,Response,TextDecoder, and Web Crypto are available for upstream HTTP, response reading, and signature verification.
ctx capability reference
Data
ctx.db.get/query/insert/update/delete operates on project JSON tables. Every write is validated against the table JSON Schema.
Authentication
ctx.auth.currentUser() reads the current user; ctx.auth.requireUser() rejects unauthenticated requests.
Cache
ctx.cache.get/set/del/incr/expire supports short-lived results, counters, and expiring state. It is not persistent storage.
Request and response
ctx.request.host/ip/method/path exposes request metadata; raw bodies follow Fetch reading semantics. Use ctx.response.status(code) to set status.
Cookies
Read, set, or delete cookies through ctx.cookies. After the first SSE event, headers are committed and cookies cannot change.
Assets
ctx.assets.upload({ filename, mimeType, base64 }) uploads runtime files and returns URL, path, and size metadata.
Streaming
ctx.sse.send(event, data) sends bounded SSE events. The platform supplies the final done or error event.
Diagnostics
Use console.log/warn/error and correlate one call with ctx.trace_id. Never log secrets or sensitive bodies.
Results and errors
The HTTP layer returns { "result": ... } or { "error": "..." }. Browser invoke() unwraps successful results and throws TalizenFuncError on failure. Return explicit objects for expected states such as no availability or duplicate submission.
import { invoke, TalizenFuncError } from 'talizen/func'
try {
const booking = await invoke('booking.create', input)
} catch (error) {
const message = error instanceof TalizenFuncError
? error.message
: 'Unable to submit. Try again later.'
}
JSON tables and persistence
A table definition must exist before writes. Definitions live at /platform/table/<key>.json, where the filename is the table key. Create or edit them with normal file tools; there is no separate table-schema tool.
{
"name": "Appointments",
"desc": "Booked appointment slots",
"json_schema": {
"type": "object",
"properties": {
"startAt": { "type": "string" },
"userId": { "type": "string" },
"status": { "type": "string" }
},
"required": ["startAt", "userId"]
}
}
The schema must be an object with non-empty properties; required can only name declared fields, and a business field cannot be named key. Writes accept declared fields only. Renaming is refused, and deletion is refused while records exist. Manage records with tools such as list_table_records and create_table_record.
Use the platform
user.idas the ownership key. Do not use email as an identity primary key or create identity tables such asusersorauth_users.
Auth, secrets, and external services
Use useAuth() for login UI; use Func auth only to protect backend actions. Read secrets with process.env.NAME. Users configure them in the Creght Backend / Env panel at panel/backend/env. Agents must not claim to manage platform env vars or place secrets in config, Func source, components, examples, comments, or generated output.
export async function main(input, ctx) {
ctx.auth.requireUser()
const response = await fetch('https://api.example.com/v1/generate', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.EXAMPLE_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok) {
ctx.response.status(502)
throw new Error('Upstream request failed')
}
return response.json()
}
For webhooks, read the raw body as required by the provider and verify its signature with Web Crypto before parsing JSON. Payments are custom server-side Func integrations: keep provider secrets in env, verify callback signatures and idempotency keys, and return explicit order states. There is no built-in payment SDK.
Asset uploads
const asset = await ctx.assets.upload({
filename: 'report.pdf',
mimeType: 'application/pdf',
base64: input.base64,
})
await ctx.db.insert('reports', {
userId: ctx.auth.requireUser().id,
url: asset.url,
path: asset.path,
size: asset.size,
})
Upload large files and persist only their URL, path, and size metadata. Do not store large base64 payloads in tables or return them in Func JSON.
Timeout configuration and diagnosis
invoke defaults to 5 seconds; the Func runner's default maximum is 300 seconds. A bounded model generation or upstream request may set a longer caller timeout. This does not enable detached background work.
const result = await invoke('image.generate', input, {
timeoutMs: 120000,
})
For context deadline exceeded or context timeout:
- Inspect the actual
invoke(..., { timeoutMs })value in the page. - Retry the same input and model with a larger timeout.
run_func.timeout_mscontrols only that self-test and does not prove the same platform hard limit. - If the larger value succeeds, raise the production caller's
timeoutMsand verify the real path.
Do not treat shorter output, lower
max_tokens, a different model/provider, a new table, or a fake background job as the timeout fix.
Native Fetch + SSE streaming
Ordinary invoke() waits for one complete JSON result. For bounded incremental output, send events with ctx.sse.send and use native browser fetch plus ReadableStream. No invokeStream wrapper is required.
// /backend/func/writer.ts
export async function main(input, ctx) {
ctx.sse.send('token', { text: 'Hello' })
ctx.sse.send('token', { text: ' world' })
return { ok: true }
}
const response = await fetch('/func/writer?stream=1&timeout_ms=120000', {
method: 'POST',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok || !response.body) throw new Error('Func stream failed')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split(/\r?\n\r?\n/)
buffer = frames.pop() || ''
for (const frame of frames) {
const event = frame.match(/^event:\s*(.+)$/m)?.[1]
const data = frame.match(/^data:\s*(.+)$/m)?.[1]
if (event === 'token' && data) {
output += JSON.parse(data).text
}
}
}
reader.read() yields arbitrary byte chunks, not complete events. Buffer across reads and split SSE frames only on a blank line. The platform finishes with done or error; timeout still applies. Cookies cannot change after the first event.
Browser calls and the SSR boundary
Call mutating Funcs from browser event handlers. Keep persistent state in Func/JSON tables rather than React state alone. The public HTTP path is /func/<key>; do not occupy /func/* with page routes.
Do not call Func from getServerSideProps. SSR exposes request/cookie helpers for public or cookie-vary-safe first-render data, but intentionally does not expose ctx.auth, ctx.func, ctx.db, or ctx.cache. Keep auth, private data, writes, and cache/database logic in Func/browser flows.
Development and acceptance workflow
- Confirm CMS or
talizen/formis insufficient. - Create or verify required JSON tables under
/platform/table. - Write ESM exports in
/backend/func, validate input, and read secrets fromprocess.env. - Use
invoke('key.method', input)from pages; use native Fetch/SSE only for streaming. - Run sample backend tests with
run_funcor the Creght CLI Func command. Remember that the test timeout applies only to that run. - Run lint after page/component edits and test success, business failure, unauthenticated, upstream failure, timeout, and stream completion paths in the real page.
- No project/site/internal table IDs enter browser payloads.
- No hard-coded secrets, legacy globals, manual dispatchers, or timers.
- Tables exist and schemas match the actual write shape.
- Protected actions call
requireUser(); records useuser.id. - Large files use asset upload; third parties, webhooks, and payments validate errors and signatures.
- Ordinary calls return structured JSON; SSE buffers frames and handles
done/error. - Caller timeout matches workload duration and the production path is verified.
