shiply.now

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

BindingWhat it is
env.ASSETSService binding to shiply-sites. Use env.ASSETS.fetch(request) to fall through to your static files.
env.SITE_DBD1 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 static

shiply 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)

ToolPurpose
deploy_functionDeploy a worker (source + optional crons)
get_functionRead the deployed source + metadata
remove_functionStrip function + secrets + crons; fall back to static
set_secret / list_secrets / remove_secretManage worker secrets
set_cron / list_crons / remove_cronManage cron triggers
get_function_logsDeep-link to the Cloudflare dashboard for live tail

REST

Method & pathPurpose
POST /api/v1/sites/{slug}/functionDeploy {source, lang?, crons?}
GET /api/v1/sites/{slug}/functionRead source + metadata
DELETE /api/v1/sites/{slug}/functionRemove 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 (or esbuild) locally and ship the pre-built worker.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.