Functions
Functions are your project's server-side code: Deno programs — standard Deno.serve handlers, deployed as-is — that run on shovelbase instead of on a server you maintain. Each function lives in its own directory with an index.ts entry point; shared code goes in _shared/.
Most of a typical app needs no backend code at all: the client talks to /rest/v1 directly and RLS decides what it may see. Functions are for the cases that model can't express — and the test for “do I need one?” is usually one of two questions: does this need a secret? or does this need to be trusted?
Anything shipped to a browser or an app binary is public, so a third-party API key can't live there — it has to run somewhere the user can't read. And a rule the client enforces isn't enforced at all, because the client is under the user's control; “only charge once” or “only admins may refund” has to live on the server. A function is that server-side place, without a server: no container to size, no host to patch, and it scales to zero when nothing calls it.
The trade-off is that a function is a network hop with a cold start. Don't wrap plain CRUD in one “to have an API layer” — that just makes queries slower and adds code to maintain, when PostgREST and RLS already do it.
- Calling a third-party API that needs a secret
- An LLM, a payment provider, an email service. The key lives in Secrets and never leaves the runtime — the client calls your function, your function calls the vendor.
- Business rules that must be trusted
- Applying a discount code, awarding credits, moving money. The client asks; the function decides, validates, and writes with the service key.
- Reacting to events
- A queue message or a cron schedule (below) triggers the same handler — nightly cleanups, digest emails, syncing an external system.
- Giving AI clients tools
- An MCP endpoint (below) is a function that exposes your data as tools Claude and other MCP clients can call, with your auth rules in front of it.
Writing and deploying one
Deno.serve(async (req: Request) => { const { messages } = await req.json(); const apiKey = Deno.env.get('OPENAI_API_KEY'); // from: shovelbase secrets set // … call your model, stream a response, etc. return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' }, });});shovelbase functions deploy chat # → https://<ref>.shovelbase.com/functions/v1/chatjsr:, npm: and URL imports work, as do streaming/SSE responses. Each function has a verify_jwt flag (default on): requests must carry a valid project JWT (the anon key counts). Deploy with --no-verify-jwt for public webhooks. Console output and errors land in Logs → Functions.
| Surface | Availability |
|---|---|
| Portal UI | Read-only code view with a verify_jwt toggle and delete; separate Schedules, MCP, and Secrets sub-pages |
| CLI | shovelbase functions deploy/list/delete, mcp list, secrets set/list/unset |
| SDK / HTTP | shovelbase.functions.invoke() / POST /functions/v1/<name> |
Injected environment
| Variable | Value |
|---|---|
SHOVELBASE_URL | The project URL — create a client inside a function to call your own database |
SHOVELBASE_ANON_KEY | Anon key |
SHOVELBASE_SERVICE_ROLE_KEY | Service key (RLS bypass) |
SHOVELBASE_DB_URL | Direct Postgres connection string |
JWT_SECRET, PROJECT_REF, S3_BUCKET | Project JWT secret, ref, storage bucket |
(your secrets) | Everything from Secrets, below, applied to all functions immediately |
SHOVELBASE_* is the canonical prefix; APP_* aliases are also injected.
Schedules
Cron-triggered runs of a project's edge functions — like pg_cron, but for functions. Create a schedule on the portal's Functions → Schedules page or from the CLI: pick the function, a cron expression, and an optional JSON payload it's invoked with. Useful for the recurring jobs a queue-triggered function can't cover — nightly syncs, cleanup, digest emails. They trigger functions the same way a queue trigger does, just on a timer instead of a message. The cron expression is five UTC fields — minute hour day month weekday. No SDK surface; manage schedules on the portal or the CLI.
shovelbase functions schedules create nightly --function report --cron "0 3 * * *" --payload '{"tz":"utc"}'shovelbase functions schedules list # cron → function timersshovelbase functions schedules enable nightly | disable nightlyshovelbase functions schedules delete nightly [--yes]Secrets
Encrypted values shared by every function in the project — an API key for a third-party service, for example. Manage them on Functions → Secrets or from the CLI; changes apply live, no redeploy needed. No SDK surface — secrets are injected into the function runtime's environment, never exposed to a client.
shovelbase secrets set OPENAI_API_KEY=sk-… # env for ALL functions, applied liveshovelbase secrets listshovelbase secrets unset OPENAI_API_KEYMCP endpoints
An MCP endpoint is a function that speaks the Model Context Protocol over Streamable HTTP, so AI clients — Claude Code, Claude Desktop, anything MCP-capable — can call tools backed by your project: query the database, trigger jobs, read storage. Same Deno runtime and deploys as any function; the --mcp flag serves it at https://<ref>.shovelbase.com/mcp/v1/<name> and lists it under the portal's Functions → MCP page.
This is how you expose your product's tools — the ones only your app knows how to provide. To let an AI client manage the project instead — run migrations, deploy this function, set its secrets — you don't write anything: every project already has a management MCP endpoint with a token you create in Settings → API.
shovelbase functions deploy portfolio --mcp # from ./shovelbase/functions/portfolio/shovelbase mcp list# portfolio auth=bearer-key https://<ref>.shovelbase.com/mcp/v1/portfolioThe portal's New MCP endpoint button deploys a working template: a stateless JSON-RPC server handling initialize, tools/list and tools/call, with a TOOLS map you extend — each tool is a name, a description, a JSON schema, and a handler that can use the project's injected env (SHOVELBASE_URL, keys, your secrets) like any function.
# verify_jwt on (default): clients authenticate with the anon keyclaude mcp add --transport http portfolio \ https://<ref>.shovelbase.com/mcp/v1/portfolio \ --header "Authorization: Bearer $ANON_KEY" # deployed with --no-verify-jwt (endpoint does its own auth): drop the headerHTTP API (/functions/v1)
curl -X POST "https://<ref>.shovelbase.com/functions/v1/chat" \ -H "Authorization: Bearer $ANON_KEY" \ -H "Content-Type: application/json" -d '{"messages": []}'See shovelbase-js for shovelbase.functions.invoke().