BackendBuild site backend workflows with Func

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 from talizen/func-runtime when needed.
  • Access every platform capability through ctx. Do not use legacy globals such as data, db, auth, or cache.
  • 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/setInterval are 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 { fileUrl, url, size }. Both URL fields contain the same value; internal storage paths are not exposed.

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.id as the ownership key. Do not use email as an identity primary key or create identity tables such as users or auth_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

Browser files: signed CDN upload

For a File or Blob selected or dropped in the browser, use talizen/assets. Do not encode it as base64 and send it through invoke() just to upload it from Func.

import { uploadAsset } from 'talizen/assets'

const asset = await uploadAsset(file, {
  onFileUploadProcess(fileName, progress) {
    console.log(fileName, progress)
  },
})

// asset.fileUrl === asset.url

uploadAsset() calls POST /api/asset/file/preupload to obtain a short-lived signed URL, uploads the file bytes directly from the browser with PUT to that CDN storage URL, then confirms the upload through POST /api/asset/file/ack. File bytes do not pass through Func, and matching content can reuse an existing object by hash.

Both File and Blob are accepted. For an unnamed Blob, call uploadAsset(blob, { fileName: 'avatar.webp' }). The result contains { fileUrl, url, fileName, mimeType, size, hash }, with identical URL fields. Signed upload works on both preview and published site domains.

Files generated inside Func

const asset = 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,
  size: asset.size,
})

Use ctx.assets.upload() only for bytes generated inside Func that cannot originate in the browser. It synchronously returns { fileUrl, url, size }, with identical URL fields. Persist the URL and size only; do not store internal paths or large base64 payloads in tables or 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:

  1. Inspect the actual invoke(..., { timeoutMs }) value in the page.
  2. Retry the same input and model with a larger timeout. run_func.timeout_ms controls only that self-test and does not prove the same platform hard limit.
  3. If the larger value succeeds, raise the production caller's timeoutMs and 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

  1. Confirm CMS or talizen/form is insufficient.
  2. Create or verify required JSON tables under /platform/table.
  3. Write ESM exports in /backend/func, validate input, and read secrets from process.env.
  4. Use invoke('key.method', input) from pages; use native Fetch/SSE only for streaming.
  5. Run sample backend tests with run_func or the Creght CLI Func command. Remember that the test timeout applies only to that run.
  6. 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 use user.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.

Render diagnostics