Atelier

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 WorkerEdge
Cold start< 500 ms< 50 ms
Timeout60 s10 s
Heap128 – 512 MB128 MB
DB accessFull read/writeRead-only
FilesystemNoNo

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