Functions (Workers Lite)
Ship a per-site Cloudflare Worker alongside your static files — webhooks, cron, secrets, authed APIs
shiply Workers Lite lets a published site include server-side code that
runs on every request to its hostname. Drop a worker.js (or
worker.ts) at the publish root and shiply publish deploys it as a
per-site Cloudflare Worker bound to <slug>.shiply.now. Use it for:
- Webhook receivers (Stripe, GitHub, etc.) with raw-body signature checks
- Cron jobs (daily reminders, periodic sync, retention emails)
- Privileged API calls using secrets without exposing them to the browser
- Authenticated mutations on D1 / Neon (auth-check in the function, then write)
Plan-gated to Developer. Founder Special and Hobby get
402 payment_required on first function deploy — point the user at
the plan page.
Hello world
// worker.ts
export default {
async fetch(request: Request, env: any): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/api/hello') {
return new Response(JSON.stringify({ ok: true, time: new Date().toISOString() }), {
headers: { 'content-type': 'application/json' },
})
}
// Fall through to static
return env.ASSETS.fetch(request)
},
}Publish:
shiply publish .The CLI uploads your static files and compiles + deploys the worker
in the same call. Hit /api/hello on the live URL and you get JSON;
every other path serves your static assets.
Bindings — what's in env
| Binding | What it is |
|---|---|
env.ASSETS | Service binding to shiply-sites. Use env.ASSETS.fetch(request) to fall through to your static files. |
env.SITE_DB | D1 binding, present when a D1 is attached to the site (shiply db attach <db> --site <slug>). |
env.<VARIABLE> | Each shiply Variable becomes a plain-text env var — visible in the function and in proxy routes. |
env.<SECRET> | Each secret set via shiply secret set is encrypted at rest and accessible as an env var. |
The runtime is a standard Cloudflare Worker V8 isolate. You get the
Cloudflare Worker globals — crypto, fetch, URL, Request,
Response, TextEncoder, TextDecoder, ReadableStream, etc. You do
not get Buffer or any Node-only API. Use Web Crypto and Web
Streams instead.
Webhook example (Stripe)
The companion runtime package shiply-runtime exports helpers tuned
for shiply functions — signature verification, JSON helpers, error
responses:
// worker.ts
import { verifyStripeSig, readJson, json, errorResponse } from 'shiply-runtime'
interface Env {
ASSETS: Fetcher
SITE_DB: D1Database
STRIPE_WEBHOOK_SECRET: string // set via: shiply secret set <slug> STRIPE_WEBHOOK_SECRET whsec_...
STRIPE_SECRET_KEY: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/api/webhooks/stripe' && request.method === 'POST') {
const body = await request.text()
const sig = request.headers.get('stripe-signature')
if (!await verifyStripeSig(body, sig, env.STRIPE_WEBHOOK_SECRET)) {
return errorResponse('bad signature', 400)
}
const event = JSON.parse(body)
if (event.type === 'invoice.payment_failed') {
await env.SITE_DB.prepare(
'INSERT INTO failed_payments (invoice_id, customer_email) VALUES (?1, ?2)'
).bind(event.data.object.id, event.data.object.customer_email).run()
}
return new Response('ok')
}
return env.ASSETS.fetch(request)
},
}The signature check reads the raw request body before any JSON parsing — Stripe (and most webhook providers) sign the unparsed bytes.
Manage from the CLI
shiply function deploy <slug> # uploads worker.js (or worker.ts with --ts) from CWD
shiply function deploy <slug> --ts # server-side TypeScript compile
shiply function get <slug> # read deployed source + metadata
shiply function rm <slug> # strip function + secrets + crons; fall back to staticshiply publish auto-detects a worker.js / worker.ts at the publish
root, so most agents won't need function deploy directly — it's there
as an alternative when you want to update the function without touching
static files.
Manage from MCP (agents)
| Tool | Purpose |
|---|---|
deploy_function | Deploy a worker (source + optional crons) |
get_function | Read the deployed source + metadata |
remove_function | Strip function + secrets + crons; fall back to static |
set_secret / list_secrets / remove_secret | Manage worker secrets |
set_cron / list_crons / remove_cron | Manage cron triggers |
get_function_logs | Deep-link to the Cloudflare dashboard for live tail |
REST
| Method & path | Purpose |
|---|---|
POST /api/v1/sites/{slug}/function | Deploy {source, lang?, crons?} |
GET /api/v1/sites/{slug}/function | Read source + metadata |
DELETE /api/v1/sites/{slug}/function | Remove function (cascades secrets + crons) |
All require Authorization: Bearer shp_….
Limits
- 1 MB compiled script size (Cloudflare's per-Worker ceiling).
- 30 s CPU per request (Cloudflare's default; isolate runtime).
- V8 isolate runtime — no Node modules, no filesystem, no native binaries.
- npm deps — zero-dep modules that target Web APIs work directly.
For anything with a bundler step, run
wrangler(oresbuild) locally and ship the pre-builtworker.js.
Pricing
Functions, cron triggers, and secrets are included in the Developer
plan. Outbound fetch() from the worker and any optional KV / R2 you
wire up bill against Cloudflare's standard metered pricing.
For schedule reference and runtime caveats see also Crons and Secrets.