Functions
Server-side TypeScript for anything that can’t run in the browser — webhooks, scheduled tasks, edge personalization, background jobs, anything that needs your DB or LLM provider directly.
Model
Drop a TypeScript file into .atelier/functions/<name>.ts in your project. Base picks it up automatically. Each invocation runs in an isolated runtime — Node Workers for general-purpose, V8 isolates for edge.
.atelier/functions/
├── process-order.ts ← on-demand HTTP
├── daily-digest.ts ← cron (schedule in frontmatter)
├── send-emails.queue.ts ← background queue worker
└── edge/
└── personalize.ts ← edge runtime
On-demand functions
The default. Invoke from your app via the SDK.
// .atelier/functions/process-order.ts
export default async function processOrder(req, ctx) {
const { orderId } = await req.json();
const { rows } = await ctx.db.query(
'SELECT * FROM orders WHERE id = $1',
[orderId]
);
if (!rows[0]) return new Response('not found', { status: 404 });
const summary = await ctx.llm.generateText({
prompt: `Summarize this order for the team: ${JSON.stringify(rows[0])}`,
});
return Response.json({ ok: true, summary });
}
import { atelier } from '@atelier/sdk';
const result = await atelier.functions.invoke('process-order', { orderId: 'ord_123' });
Scheduled functions (cron)
Declare a cron schedule in export const schedule; Base runs it automatically.
// .atelier/functions/daily-digest.ts
export const schedule = '0 9 * * *'; // every day at 09:00 UTC
export default async function dailyDigest(_req, ctx) {
const { rows } = await ctx.db.query(
`SELECT user_id, count(*) AS n FROM todos
WHERE done = false GROUP BY user_id`
);
// ... send email summaries
return Response.json({ users: rows.length });
}
Drift is typically under 30 seconds. Missed runs (during deploy windows) replay automatically.
Edge functions
For read-heavy paths where cold-start matters, opt into the edge runtime (V8 isolates). Cold start typically under 50 ms.
// .atelier/functions/edge/personalize.ts
export const runtime = 'edge';
export default async function personalize(req, ctx) {
const country = req.headers.get('x-vercel-ip-country') ?? 'US';
return Response.json({ greeting: greetings[country] });
}
The edge runtime has a smaller capability surface: read-only DB binding, no filesystem, no long-lived connections.
Background queues
Durable, retryable jobs with backoff. Define a queue worker and enqueue from anywhere.
// .atelier/functions/send-emails.queue.ts
export const queue = { concurrency: 4, retries: 5, backoff: 'exponential' };
export default async function sendEmail(job, ctx) {
const { userId, template } = job.payload;
// ... call email provider
}
import { atelier } from '@atelier/sdk';
await atelier.queue('send-emails').enqueue({ userId, template: 'welcome' });
Bindings — the ctx object
Every function receives a ctx argument with bindings to your project resources:
ctx.db.query(sql, params)— parameterized SQL against your project database. RLS context is threaded from the caller’s session.ctx.llm.generateText(opts),ctx.llm.streamText(opts),ctx.llm.embed(opts)— calls the user’s BYOS provider through the desktop tunnel.ctx.user— caller’s identity (when invoked from an authenticated session).ctx.env— project environment variables (set in the Base console).ctx.storage.upload(file),ctx.storage.read(fileId)— direct storage access.ctx.vector.upsert(items),ctx.vector.search(query)— vector search bindings.
Limits
| Node Worker | Edge | |
|---|---|---|
| Cold start | < 500 ms | < 50 ms |
| Timeout | 60 s | 10 s |
| Heap | 128 – 512 MB | 128 MB |
| DB access | Full read/write | Read-only |
| Filesystem | No | No |
Local development
Functions run in the same dev server as your app. Hot reload, full stack traces, breakpoints. The ctx bindings hit your local PGlite and your real BYOS provider.
atelier dev
# functions hot-reload on save