shiply-cli 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ ---
2
+ type: reference
3
+ title: Client work (projects, clients, contracts, marketplace)
4
+ description: Freelancer delivery — customer intake projects with AI briefs, grouping work by client, e-sign contracts with amendments, and selling built sites on the marketplace via Stripe Connect.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # Client work — projects, clients, contracts, marketplace
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). All of it Bearer-
11
+ key authenticated.
12
+
13
+ ## Projects — customer intake + AI brief
14
+
15
+ When a dev needs to capture a real client brief, they spin up a **project**:
16
+ the dashboard (or `shiply project create`) mints a one-URL intake form to
17
+ share with the customer. The customer fills a 10-step wizard, attaches
18
+ files, hits submit. An AI brief generator (MiniMax-M2 with Anthropic
19
+ fallback) turns the answers into a structured brief the dev reviews and
20
+ edits inline. All files flow into a per-project drive folder.
21
+
22
+ MCP tools:
23
+ ```
24
+ list_projects — your projects (filter status/q)
25
+ create_project — start a new intake; pass customerName/customerEmail to email them the link
26
+ get_project — full project + brief
27
+ update_brief — patch brief jsonb (after AI generation)
28
+ regenerate_brief — re-run AI from current intake responses
29
+ archive_project / restore_project — lifecycle
30
+ list_project_files — files uploaded by the customer
31
+ resend_intake_invite — re-email the customer their intake link (requires customerEmail on project)
32
+ ```
33
+
34
+ REST: `POST/GET /api/v1/projects · GET/PATCH /api/v1/projects/{id} ·
35
+ POST /api/v1/projects/{id}/regenerate-brief`
36
+ (archive: `PATCH /api/v1/projects/{id}` with `{"status":"archived"}` — no
37
+ dedicated `/archive` route).
38
+
39
+ CLI: `shiply project ls · shiply project create <label> [--customer-email <e>]
40
+ [--customer-name <n>] · shiply project get <id> · shiply project archive <id>`
41
+
42
+ Docs: https://shiply.now/docs/projects
43
+
44
+ ## Group work by client (optional, for freelancers)
45
+
46
+ Building sites for clients? Pass an optional `client` (a name or email) on any
47
+ `publish_site`, `create_drive`, or `create_project` call — shiply
48
+ create-or-finds the client by email and files everything you ship for them
49
+ (sites, drop-box, intake, contracts) under one customer view in the dashboard.
50
+ A project's customer **is** the site's client (same email-keyed identity), so
51
+ `create_project` already links automatically whenever you pass
52
+ `customerEmail` — no extra step. There is no `create_client` tool and never a
53
+ CRM step: the client record self-assembles from the calls you already make.
54
+ `publish_site` echoes the `resolvedClientId` — reuse it across the session so
55
+ drive/intake/contract all land under the same client. List calls take an
56
+ optional `clientId` filter (`list_sites`/`list_drives`/`list_projects`).
57
+ Omit `client` for personal projects; shiply works identically.
58
+ CLI: `--client "<name-or-email>"` on publish/drive/project, plus
59
+ `shiply client ls|show`.
60
+
61
+ ## Contracts — draft, sign, amend (extends Projects)
62
+
63
+ Once a project reaches `brief_ready`, the dev drafts a signed contract from
64
+ the brief, the customer e-signs in the portal (typed-name + checkbox), and
65
+ the dev amends it later as scope evolves. A signed contract is immutable
66
+ — any change is a new amendment row chained off the parent. The portal
67
+ sends the customer; the dev can resend / remind / retract. A 7-day
68
+ reminder cron nudges unsigned contracts automatically.
69
+
70
+ MCP tools:
71
+ ```
72
+ contract_draft — draft a contract from a brief_ready project (returns the new contract id)
73
+ contract_send — flip status='sent', fire customer email (parent OR amendment)
74
+ contract_amend — create an amendment draft on a SIGNED parent (scopeDelta required, fee + date optional)
75
+ contract_status — read state + amendments (poll this to see status='signed')
76
+ contract_pdf — base64-encoded signed PDF (parent + cert + signed amendments)
77
+ ```
78
+
79
+ Edit-before-send today is REST-only: `PATCH /api/v1/contracts/{id}` lets
80
+ the dev tweak the 8 fields (scopeSummary, feeCents, currency, dates,
81
+ revisionCount, revisionOverageCents, jurisdiction). After PATCH, call
82
+ `contract_send` to fire it. Amendments are sent the same way (call
83
+ `contract_send` with the amendment id returned by `contract_amend`).
84
+
85
+ Example agent flow (draft → tweak fee → send → poll → amend):
86
+ 1. `contract_draft({projectId})` → returns `{id, scopeSummary, feeCents:null, ...}`
87
+ 2. `PATCH /api/v1/contracts/{id}` `{feeCents: 450000}` (Bearer key)
88
+ 3. `contract_send({contractId: id})` → project flips to `contract_sent`
89
+ 4. Poll `contract_status({contractId: id})` until `contract.status === 'signed'`
90
+ 5. `contract_amend({parentContractId: id, scopeDelta: "Add login flow", feeDeltaCents: 50000})` → returns the amendment row
91
+ 6. `contract_send({contractId: amendment.id})` (no PATCH needed if the amend fields are right)
92
+
93
+ REST (Bearer for dev, portal cookie for customer):
94
+ `POST /api/v1/projects/{id}/contracts` (draft) ·
95
+ `GET/PATCH /api/v1/contracts/{id}` (read either-party / dev edit) ·
96
+ `POST /api/v1/contracts/{id}/send` (dev) ·
97
+ `POST /api/v1/contracts/{id}/view` (customer ping) ·
98
+ `POST /api/v1/contracts/{id}/sign` (customer e-sign) ·
99
+ `POST /api/v1/contracts/{id}/retract` (dev; `?recoverAsDraft=true` to
100
+ recover the draft instead of voiding) ·
101
+ `POST /api/v1/contracts/{id}/amend` (dev; signed parents only) ·
102
+ `POST /api/v1/contracts/{id}/remind` ·
103
+ `POST /api/v1/contracts/{id}/resend` (manual nudges, rate-limited 3/24h) ·
104
+ `GET /api/v1/contracts/{id}/pdf` (binary, signed only).
105
+
106
+ `contract_pdf` returns `{filename, contentType:"application/pdf", base64,
107
+ byteLength}` so the MCP client can decode and save the bytes.
108
+
109
+ CLI: `shiply contract list <project-id>`, `shiply contract draft
110
+ <project-id>`, `shiply contract show <contract-id>`, `shiply contract send
111
+ <contract-id>`, `shiply contract pdf <contract-id> -o file.pdf`, `shiply
112
+ contract amend <id> --scope "…" [--fee-delta N] [--target-date YYYY-MM-DD]`,
113
+ `shiply contract retract <id> [--keep-as-draft]`.
114
+
115
+ Docs: https://shiply.now/docs/contracts
116
+
117
+ ## Marketplace — sell built sites
118
+
119
+ Devs can list any owned site for sale: set a price + short pitch, choose
120
+ standard or custom terms, pick a jurisdiction. Stripe Connect Express
121
+ handles seller payouts — sellers complete a one-time onboarding before
122
+ listing (`get_connect_status` returns the onboarding URL when needed).
123
+ Buyers pay via Stripe Checkout; on `checkout.session.completed` the
124
+ webhook flips the order to `paid` and transfers site ownership
125
+ atomically. Refunds are available within the order's refund window
126
+ (`refundExpiresAt`); a refund reverts ownership to the seller.
127
+
128
+ Prices are passed as `priceCents` (whole-dollar cents between 100 and
129
+ 999900). `termsMode='standard'` uses shiply's template; `'custom'`
130
+ requires `termsCustom` ≥50 chars.
131
+
132
+ MCP tools:
133
+ ```
134
+ list_listings — my listings
135
+ create_listing — list a site (siteSlug, priceCents, pitch?, termsMode, jurisdiction, ...)
136
+ update_listing — change price/pitch/status (draft|live|paused)
137
+ delete_listing — pulls off marketplace (sets status=draft)
138
+ list_my_sales — orders where I'm the seller
139
+ list_my_orders — orders where I'm the buyer
140
+ refund_order — issue Stripe refund (within refund window; goes back to buyer)
141
+ get_connect_status — Stripe Connect onboarding state + actionable URL
142
+ ```
143
+
144
+ REST: `POST /api/v1/listings · PATCH /api/v1/listings/{id} ·
145
+ POST /api/v1/listings/{id}/checkout · POST /api/v1/orders/{id}/refund ·
146
+ GET /api/v1/connect/status`
147
+ (no GET listing index over REST yet — use the MCP `list_listings` /
148
+ `list_my_sales` / `list_my_orders` tools for machine output, or the
149
+ dashboard `/dashboard/sales` page for humans.)
150
+
151
+ CLI: `shiply listing ls · shiply listing create <site-slug> --price <cents>
152
+ --jurisdiction "<region>" · shiply listing rm <slug> --id <listing-id>`
153
+
154
+ **Stripe Connect note:** Sellers must complete one-time Stripe Connect
155
+ onboarding before listing. Call `get_connect_status` first — when status
156
+ isn't `'ready'` it returns an `onboardingUrl` you should hand to the
157
+ user as a clickable link.
158
+
159
+ Docs: https://shiply.now/docs/marketplace
@@ -0,0 +1,87 @@
1
+ ---
2
+ type: reference
3
+ title: Custom domains
4
+ description: Serve a site at the user's own domain — one-click OAuth DNS (Cloudflare, GoDaddy, IONOS), manual CNAME fallback, subdomain mapping, primary-hostname SEO, readiness polling.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # Custom domains
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). Serve a site at
11
+ `www.yourdomain.com` instead of a `*.shiply.now` URL. Requires a paid plan and
12
+ a Bearer API key.
13
+
14
+ ## The agent sequence (do it in this order)
15
+
16
+ 1. Confirm the user is authenticated and on a paid plan (`whoami` over MCP, or
17
+ `shiply status`).
18
+ 2. Add the root domain — this registers it and detects the DNS provider
19
+ (cloudflare, godaddy, ionos, or manual).
20
+ 3. **Supported provider detected** → start the one-click OAuth connect; it
21
+ returns a URL. SHOW THE USER that URL and ask them to open it in their
22
+ browser to authorize DNS writes. Wait for their confirmation.
23
+ **Provider is "manual"** → show the user the CNAME record returned by
24
+ step 2 (target: `cname.shiply.now`) and ask them to add it at their
25
+ registrar. Wait for confirmation.
26
+ 4. Map the subdomain to the site. The first subdomain you add for a site is
27
+ primary by default; later mappings start non-primary and 301-redirect to it.
28
+ 5. (Optional, SEO) Set the primary hostname between apex and www — sibling
29
+ hostnames 301-redirect to it, preserving path + query. Fixes duplicate
30
+ content.
31
+ 6. Poll the check endpoint until `ready: true` (usually 1–5 minutes), then
32
+ tell the user their domain is live.
33
+
34
+ ## CLI commands
35
+
36
+ ```bash
37
+ shiply domain add example.com # register a root domain (detects provider)
38
+ shiply domain connect example.com # one-click OAuth DNS setup (Cloudflare, GoDaddy, IONOS, etc.)
39
+ shiply domain sub add www.example.com --site my-site # map a subdomain → site
40
+ shiply domain ls # list all domains + subdomains + status
41
+ shiply domain sync example.com # re-sync DNS records with a connected provider
42
+ shiply domain primary <hostname> # mark hostname canonical (siblings 301)
43
+ ```
44
+
45
+ For one-click connect (`shiply domain connect`), the CLI prints an
46
+ authorization URL — the user must open it in a browser and approve the OAuth
47
+ grant before DNS is written.
48
+
49
+ For unsupported providers, `shiply domain add` returns the CNAME record to add
50
+ manually: `www CNAME cname.shiply.now`
51
+
52
+ Check status after connecting:
53
+ ```bash
54
+ shiply status www.example.com --wait # polls until SSL_READY + SITE_READY; exit 0 when live
55
+ ```
56
+
57
+ ## MCP tools
58
+
59
+ | Tool | Purpose |
60
+ |------|---------|
61
+ | `add_custom_domain` | Add a root domain and detect the provider |
62
+ | `connect_provider` | Start OAuth → returns `{ url }` (show to user to authorize) |
63
+ | `add_subdomain` | Map a subdomain to a site slug |
64
+ | `set_primary_subdomain` | Pick the canonical URL; siblings 301-redirect (SEO fix) |
65
+ | `check_custom_domain` | Live CNAME + SSL + readiness probe |
66
+ | `sync_dns` | Re-push DNS records to a connected provider |
67
+ | `list_custom_domains` | List all domains and their subdomain mappings |
68
+ | `remove_custom_domain` | Remove a domain and all its mappings |
69
+
70
+ ## REST endpoints (`/api/v1/custom-domains`)
71
+
72
+ ```
73
+ GET /api/v1/custom-domains list domains
74
+ POST /api/v1/custom-domains add domain { domain }
75
+ DELETE /api/v1/custom-domains/{domain} remove domain
76
+ POST /api/v1/custom-domains/{domain}/subdomains add subdomain { subdomain, slug }
77
+ POST /api/v1/custom-domains/{domain}/subdomains/{id}/primary mark hostname canonical (siblings 301)
78
+ POST /api/v1/custom-domains/{domain}/connect start OAuth → { url }
79
+ POST /api/v1/custom-domains/{domain}/sync-dns re-sync DNS
80
+ POST /api/v1/custom-domains/{domain}/check live status → {cname, ssl:{valid,issuer,daysLeft}, http, ready}
81
+ ```
82
+
83
+ All require `Authorization: Bearer shp_…`. Poll the check endpoint until
84
+ `ready:true` before telling the user it's done.
85
+
86
+ Vanity handle (paid, no domain needed): `POST /api/v1/publish/<slug>/handle
87
+ {"handle"}` → `<handle>.shiply.now`. MCP: `set_handle`.
@@ -0,0 +1,108 @@
1
+ ---
2
+ type: reference
3
+ title: Databases (D1 + Neon Postgres)
4
+ description: Per-site SQL — free Cloudflare D1 (SQLite at the edge, browser query shim) and Neon Postgres (developer plan, copy-on-write branching), migrations, attach, per-DB MCP server.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # SQL databases (Cloudflare D1 + Neon Postgres)
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). Two engines, same
11
+ CLI/REST/MCP surface. **D1 (SQLite at the edge)** is free on every plan and
12
+ queryable from the browser via a built-in fetch shim — no API key in the page.
13
+ **Neon Postgres** is a developer-plan add-on with copy-on-write branching; the
14
+ connection URI is encrypted at rest and surfaced to the site's Worker as
15
+ `env.DATABASE_URL`.
16
+
17
+ ## D1 (default)
18
+
19
+ ```bash
20
+ shiply db create app # provision a D1 (binding SITE_DB)
21
+ shiply db ls # list (name, provider, size, attached site)
22
+ shiply db sql app "SELECT 1" # one-shot query against your account
23
+ shiply db sql app "SELECT * FROM t WHERE id=?1" --params '[42]'
24
+ shiply db migrate app ./migrations # apply every *.sql in dir, sorted
25
+ shiply db attach app --site my-site # bind an existing DB to another site
26
+ shiply db delete app --yes # drop the DB and every row
27
+ ```
28
+
29
+ Run `shiply db create` inside a publish directory: it records the new
30
+ `databaseId` into `.shiply.json` so the next `shiply publish` auto-attaches
31
+ it. Site code then calls the shim:
32
+
33
+ ```js
34
+ fetch('/_shiply/db/SITE_DB/query', {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify({ sql: 'SELECT * FROM posts', params: [] }),
38
+ }).then(r => r.json()) // → { results: [...], meta: {rows_read, rows_written, duration} }
39
+ ```
40
+
41
+ **Public by default**: the shim has no visitor auth, so anyone who can fetch
42
+ the site can run any SQL. Until a private-mode gate ships, recommend it
43
+ strictly as a SELECT-only public data layer. Reads always work; writes hit
44
+ 402 once the DB exceeds the plan storage cap (Founder Special 100 MB,
45
+ Hobby 1 GB, Developer 10 GB).
46
+
47
+ D1 caveats: SQLite dialect, no cross-DB joins, 10 GB per-DB ceiling.
48
+
49
+ ## Neon Postgres (developer plan)
50
+
51
+ Pass `--postgres` on create. One isolated Neon project per database;
52
+ binding defaults to `DATABASE_URL`. Free / Hobby get `402
53
+ payment_required` — point the user at https://shiply.now/dashboard/plan.
54
+
55
+ ```bash
56
+ shiply db create app --postgres # provision Neon (~5–10s)
57
+ shiply db branch app dev # copy-on-write branch off main
58
+ shiply db branches app # list branches
59
+ shiply db delete-branch dev --yes # drop branch + endpoint
60
+ shiply db merge dev # NO-OP — prints pg_dump tip (see below)
61
+ ```
62
+
63
+ After the next `shiply publish` the serving Worker receives
64
+ `env.DATABASE_URL`. Minimal example:
65
+
66
+ ```js
67
+ import { neon } from '@neondatabase/serverless'
68
+ const sql = neon(process.env.DATABASE_URL)
69
+ const rows = await sql`SELECT now()`
70
+ ```
71
+
72
+ Anything that speaks Postgres works: `pg`, `postgres`, drizzle, kysely,
73
+ prisma (with the Neon adapter). Connection URIs are AES-256-GCM encrypted
74
+ at rest.
75
+
76
+ **Branching is the headline feature.** Branches are cheap, instant,
77
+ copy-on-write forks of `main`. Bind a publish to a specific branch with
78
+ `shiply publish --preview-branch=<branchDbId>` (use the `site_databases`
79
+ row id printed by `shiply db branch`, NOT the raw Neon branch id) —
80
+ great for preview deploys and safe schema rollouts.
81
+
82
+ **`shiply db merge` is intentionally a no-op.** Neon has no server-side
83
+ merge. To promote branch data into the parent:
84
+ `pg_dump "$BRANCH_DATABASE_URL" | psql "$PARENT_DATABASE_URL"`.
85
+ Run schema migrations against both branches to keep them aligned.
86
+
87
+ **Not yet supported on Neon**: branch-of-branch (branches off `main`
88
+ only), auto preview-branch on every publish, cross-region projects.
89
+
90
+ ## REST + MCP (provider-agnostic)
91
+
92
+ REST (Bearer `shp_…`): `POST /api/v1/databases` (pass `"provider":"neon"`
93
+ for Postgres) to create, `POST /api/v1/databases/{id}/query` to query (works
94
+ for D1 and Neon), `POST .../attach` to bind, `DELETE` to drop. Neon-only:
95
+ `POST/GET/DELETE /api/v1/databases/{id}/branches[/{branchId}]` (400 on
96
+ D1 rows).
97
+
98
+ Per-DB MCP server (so an agent can be scoped to one DB):
99
+ `https://shiply.now/api/mcp/db/<id>/sse` (or `/mcp` for streamable-http),
100
+ Bearer `shp_…` — tools: `db_query`, `db_list_tables`, `db_schema`. The same
101
+ three tools work against both providers; `db_schema` introspects
102
+ `sqlite_master` (D1) or `information_schema` (Neon).
103
+
104
+ Plan quotas: every account gets 1 free D1 (Founder Special: 100 MB; Hobby:
105
+ 5 × 1 GB; Developer: unlimited × 10 GB). Size sampled every 15 min; writes
106
+ blocked at 402 over plan cap; reads still work.
107
+
108
+ Full docs: https://shiply.now/docs/databases
@@ -0,0 +1,147 @@
1
+ ---
2
+ type: reference
3
+ title: Email (capture, send, inbox, broadcast, sending domains)
4
+ description: Every owned site has a real email address — public signup capture with double opt-in, authenticated send, inbox reading, audience broadcasts, and BYO verified sending domains.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # Agent Email — inbox, send, capture, broadcast (built into every owned site)
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). Every owned site
11
+ has a real email address and can send, receive, capture signups, and broadcast
12
+ — in one line. Like AgentMail, built into the site.
13
+
14
+ ## Capture (public form, zero-config — but the SITE must be owned)
15
+
16
+ ```
17
+ POST /.shiply/email { "email": "user@example.com", ...anyExtraFields }
18
+ ```
19
+
20
+ No manifest needed. Works on the **relative path** from the published page.
21
+ On success: record lands in the site inbox + owner gets a notification ping +
22
+ a double-opt-in confirmation email goes to the visitor (all on by default).
23
+ **Owned sites only** — anonymous sites get `403 email_requires_account` (claim
24
+ the site first). A non-empty `company` field is treated as a honeypot (bot
25
+ submissions are silently dropped). Caps: 16 KB body, 30 fields max.
26
+
27
+ ## Send (authenticated — Bearer key only)
28
+
29
+ The public page can NEVER send; only authenticated calls can.
30
+
31
+ ```
32
+ POST https://shiply.now/api/v1/email/send
33
+ Authorization: Bearer shp_…
34
+ { "slug": "my-site", "to": "user@example.com", "subject": "Hi", "html": "<p>Hi</p>", "text": "Hi" }
35
+ → { "messageId": "..." }
36
+ ```
37
+
38
+ ## Read inbox
39
+
40
+ ```
41
+ GET https://shiply.now/api/v1/email/inbox # all sites
42
+ GET https://shiply.now/api/v1/email/inbox?slug=my-site
43
+ Authorization: Bearer shp_…
44
+ ```
45
+
46
+ ## Typed upgrade (optional)
47
+
48
+ Declare an `email` block on a `.shiply/data.json` collection for typed fields +
49
+ fine-grained control (see [site-features.md](site-features.md) for Site Data):
50
+
51
+ ```json
52
+ {
53
+ "collections": {
54
+ "signups": {
55
+ "fields": { "email": { "type": "email", "required": true } },
56
+ "email": {
57
+ "emailField": "email",
58
+ "confirm": true,
59
+ "notify": true,
60
+ "audience": true
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ All three flags default to `true`. With this manifest, POST to
68
+ `/.shiply/data/signups` as usual — the `email` block activates the email
69
+ layer on that collection.
70
+
71
+ ## Confirm / unsubscribe (public — in the confirmation email)
72
+
73
+ ```
74
+ GET /api/email/<slug>/<collection>/confirm?token=<token>
75
+ GET /api/email/<slug>/<collection>/unsubscribe?email=<address>
76
+ ```
77
+
78
+ ## Audience + broadcast
79
+
80
+ Confirmed (double-opt-in) signups form a list. Broadcast to them:
81
+
82
+ ```
83
+ POST /api/v1/publishes/<slug>/mailboxes/<collection>/broadcast
84
+ Authorization: Bearer shp_…
85
+ { "subject": "Launch day", "html": "<p>We're live!</p>" }
86
+ ```
87
+
88
+ Spam-checked before send; unsubscribe footer is auto-added; requires ≥1
89
+ confirmed subscriber.
90
+
91
+ Configure a mailbox: `GET/PATCH /api/v1/publishes/<slug>/mailboxes/<collection>`
92
+ List contacts: `GET /api/v1/publishes/<slug>/mailboxes/<collection>/contacts?status=confirmed`
93
+
94
+ ## MCP tools
95
+
96
+ | Tool | Purpose |
97
+ |---|---|
98
+ | `send_email` | Send transactional email from a site (Bearer) |
99
+ | `list_site_inbox` | Read the inbox (all threads or filter by slug) |
100
+ | `set_mailbox` | Configure a collection's mailbox settings |
101
+ | `list_mailbox_contacts` | List audience contacts (filter by status) |
102
+ | `send_mailbox_broadcast` | Broadcast to confirmed audience (spam-checked) |
103
+
104
+ ## CLI
105
+
106
+ ```bash
107
+ shiply email send --site <slug> --to <address> --subject <subject> --html <html>
108
+ shiply email inbox [--site <slug>]
109
+ shiply mailbox set <slug> <collection> [--confirm] [--notify] [--from <domain>]
110
+ shiply mailbox broadcast <slug> <collection> --subject <subject> --html <html>
111
+ ```
112
+
113
+ Docs: https://shiply.now/docs/agent-email
114
+
115
+ ## Email demand tests (agent-managed — no Resend account needed)
116
+
117
+ Validate an idea end-to-end: `create_test({ idea, headline, sub?, cta?,
118
+ price? })` (MCP) deploys a capture landing page + provisions a confirmed-
119
+ subscriber segment; returns `testId` + live `siteUrl`. Share the siteUrl.
120
+ Visitors submit the form → shiply sends double-opt-in confirmation
121
+ automatically → they click to confirm. `get_test_status({ testId })` → ONE
122
+ merged object: funnel (views/signups/confirmed/conversionRate), email
123
+ (delivered/opened/clicked/bounced), verdict (signal:
124
+ strong|weak|inconclusive, confirmedSignupRate). CONFIRMED is the real demand
125
+ signal (double opt-in). `send_broadcast({ testId, subject, html })` emails
126
+ the confirmed audience (unsubscribe link auto-added). `list_tests()` — all
127
+ tests for this key. `resend_confirmation({ testId, email })` — re-send the
128
+ opt-in to one address.
129
+
130
+ ## Sending domains — outbound email on your domain
131
+
132
+ For agents that want shiply to send emails (demand-test broadcasts, project
133
+ intake invites, transactional notifications) from a custom verified domain
134
+ instead of the shared shiply pool. Backed by Resend — add the DNS records
135
+ they return at your registrar, then verify to re-check.
136
+
137
+ MCP tools: `list_sending_domains`, `add_sending_domain` (returns DNS records
138
+ to add), `verify_sending_domain`, `remove_sending_domain`.
139
+
140
+ REST: `GET/POST /api/v1/sending-domains ·
141
+ POST /api/v1/sending-domains/{id}/verify · DELETE /api/v1/sending-domains/{id}`
142
+
143
+ CLI: `shiply sending-domain ls · shiply sending-domain add <domain> ·
144
+ shiply sending-domain verify <id> · shiply sending-domain rm <id> --yes`
145
+
146
+ Cannot be a `shiply.now` subdomain. Verified status flips once SPF +
147
+ DKIM (+ MX for inbound) all check out.
@@ -0,0 +1,100 @@
1
+ ---
2
+ type: reference
3
+ title: Functions (Workers Lite)
4
+ description: Server-side worker.js/worker.ts on every request — webhooks with signature verification, cron triggers, encrypted secrets, runtime logs. Developer plan.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # Functions (Workers Lite) — webhooks, cron, secrets, full backend
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). Plan-gated to
11
+ Developer: Founder/Hobby see `402 payment_required`
12
+ (upgrade: shiply.now/dashboard/plan).
13
+
14
+ shiply Workers Lite lets a published site include server-side code that runs
15
+ on every request. Use when the user needs:
16
+ - A webhook receiver (Stripe, GitHub, etc.) with raw body + signature verification
17
+ - A cron job (daily reminders, periodic sync, retention emails)
18
+ - A privileged API call using secrets (Stripe key, OpenAI key) without exposing them to the browser
19
+ - An authenticated mutation on D1 / Neon (do the auth check in the function before writing)
20
+
21
+ ## How it works
22
+
23
+ Author `worker.js` (or `worker.ts`) at the publish root. On `shiply publish`,
24
+ shiply compiles (TS) + deploys it as a per-site Cloudflare Worker bound to
25
+ the site's hostname. The worker handles every request; pass static asset
26
+ requests through `env.ASSETS.fetch(request)`.
27
+
28
+ ```ts
29
+ import { verifyStripeSig, json } from 'shiply-runtime'
30
+
31
+ export default {
32
+ async fetch(req: Request, env: Env): Promise<Response> {
33
+ const url = new URL(req.url)
34
+ if (url.pathname === '/api/webhooks/stripe' && req.method === 'POST') {
35
+ const body = await req.text()
36
+ if (!await verifyStripeSig(body, req.headers.get('stripe-signature'), env.STRIPE_WEBHOOK_SECRET)) {
37
+ return new Response('bad sig', { status: 400 })
38
+ }
39
+ // ... handle event, write to env.SITE_DB
40
+ return new Response('ok')
41
+ }
42
+ return env.ASSETS.fetch(req)
43
+ },
44
+ async scheduled(event: ScheduledEvent, env: Env) {
45
+ if (event.cron === '0 9 * * *') { /* daily job */ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ `.shiply/crons.json` declares cron schedules. `.shiply/secrets.json` declares
51
+ secret names (values set via CLI/MCP, never reach the publish payload).
52
+
53
+ Runtime: V8 isolate — no Node APIs, no Buffer; use Web Crypto + Web Fetch.
54
+ Companion npm package `shiply-runtime` exports `verifyStripeSig`, `readJson`,
55
+ `json`, `errorResponse`.
56
+
57
+ ## Bindings available in `env`
58
+
59
+ - `env.ASSETS` — fall through to static (`env.ASSETS.fetch(request)`)
60
+ - `env.SITE_DB` — attached D1 (if `shiply db attach` was run)
61
+ - `env.<NAME>` — each shiply Variable becomes a plain-text env var
62
+ - `env.<NAME>` — each secret set via `set_secret` is encrypted, accessible as env var
63
+
64
+ ## MCP tools
65
+
66
+ | Tool | Purpose |
67
+ |---|---|
68
+ | `deploy_function` | Deploy a Worker function (alternative to publish auto-detection) |
69
+ | `get_function` | Read the deployed source + metadata |
70
+ | `remove_function` | Strip function + secrets + crons; fall back to static |
71
+ | `set_secret` / `list_secrets` / `remove_secret` | Manage CF Worker secrets |
72
+ | `set_cron` / `list_crons` / `remove_cron` | Manage cron triggers |
73
+ | `get_function_logs` | Read recent runtime logs (CF Observability, 7-day, newest-first) + a dashboard deep-link |
74
+
75
+ ## CLI
76
+
77
+ ```bash
78
+ shiply publish . # auto-detects worker.js + crons.json + secrets.json
79
+ shiply function deploy <slug> # alternative: upload worker.js without re-publishing
80
+ shiply function deploy <slug> --ts # uploads worker.ts (server-side compile)
81
+ shiply secret set <slug> STRIPE_KEY sk_xxx # set secret value
82
+ shiply cron set <slug> /api/daily "0 9 * * *"
83
+ shiply logs <slug> # recent worker logs (newest-first); --limit N --since MIN --json
84
+ ```
85
+
86
+ ## REST
87
+
88
+ `POST/GET/DELETE /api/v1/sites/{slug}/function` (body: {source, lang?, crons?})
89
+ · `POST/GET /api/v1/sites/{slug}/secrets` {"name","value"}
90
+ · `DELETE /api/v1/sites/{slug}/secrets/{name}`
91
+ · `GET/POST/DELETE /api/v1/sites/{slug}/crons` {"path","schedule"}
92
+ · `GET /api/v1/sites/{slug}/logs?limit&since` (recent runtime logs, newest-first)
93
+
94
+ ## Limits + plan
95
+
96
+ - 1 MB compiled script size, 30s CPU per request, V8 isolate runtime (no Node APIs)
97
+ - 50 secrets, 20 cron triggers per site
98
+ - Plan-gated to Developer
99
+
100
+ Docs: https://shiply.now/docs/functions