shiply-cli 0.7.1 → 0.12.0

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.
package/dist/login.js ADDED
@@ -0,0 +1,60 @@
1
+ import { hostname } from 'node:os';
2
+ import { api, ApiError } from './publish.js';
3
+ /** RFC 8628 client. The default for `shiply login`: no email round-trip,
4
+ * no manual code paste — just open a URL, click Allow, return to the
5
+ * terminal. The user can override the displayed agent name with --name
6
+ * (otherwise we synthesize from hostname). */
7
+ export async function loginViaDeviceFlow(base, agentNameOverride) {
8
+ const agentName = (agentNameOverride ?? `shiply-cli on ${hostname()}`).trim().slice(0, 80);
9
+ const start = await api(`${base}/api/v1/auth/device/start`, {
10
+ method: 'POST',
11
+ headers: { 'content-type': 'application/json' },
12
+ body: JSON.stringify({ agent_name: agentName }),
13
+ });
14
+ // Print the URL prominently — the user must open it. We don't auto-launch
15
+ // a browser because the CLI runs in headless contexts (CI, SSH, containers)
16
+ // where opening a local browser would either fail silently or open in the
17
+ // wrong machine. Showing the URL is the predictable thing.
18
+ console.log('');
19
+ console.log(` → Open this URL to authorize ${agentName}:`);
20
+ console.log('');
21
+ console.log(` ${start.verification_url}`);
22
+ console.log('');
23
+ console.log(` Code shown there: ${start.user_code}`);
24
+ console.log(` Waiting up to ${Math.round(start.expires_in / 60)} minutes…`);
25
+ const deadline = Date.now() + start.expires_in * 1000;
26
+ const intervalMs = Math.max(start.interval, 1) * 1000;
27
+ while (Date.now() < deadline) {
28
+ await sleep(intervalMs);
29
+ let poll;
30
+ try {
31
+ poll = await api(`${base}/api/v1/auth/device/poll`, {
32
+ method: 'POST',
33
+ headers: { 'content-type': 'application/json' },
34
+ body: JSON.stringify({ device_code: start.device_code }),
35
+ });
36
+ }
37
+ catch (e) {
38
+ // Transient network blip — don't kill the whole flow, the user has time.
39
+ if (e instanceof ApiError && e.status >= 500)
40
+ continue;
41
+ throw e;
42
+ }
43
+ if (poll.status === 'approved' && poll.api_key) {
44
+ return { ok: true, apiKey: poll.api_key, slug: poll.slug_claimed ?? null };
45
+ }
46
+ if (poll.status === 'denied')
47
+ return { ok: false, reason: 'denied by you in the browser' };
48
+ if (poll.status === 'expired')
49
+ return { ok: false, reason: 'authorization request expired' };
50
+ if (poll.status === 'consumed') {
51
+ // Another process already collected the key for this device_code; we
52
+ // can't recover it. Tell the user to retry.
53
+ return { ok: false, reason: 'this authorization was already collected elsewhere — retry to mint a fresh one' };
54
+ }
55
+ }
56
+ return { ok: false, reason: 'authorization timed out — re-run `shiply login` to try again' };
57
+ }
58
+ function sleep(ms) {
59
+ return new Promise((r) => setTimeout(r, ms));
60
+ }
package/dist/publish.js CHANGED
@@ -40,6 +40,8 @@ export async function publish(dir, opts = {}) {
40
40
  ...(opts.spaMode ? { spaMode: true } : {}),
41
41
  ...(opts.claimToken ? { claimToken: opts.claimToken } : {}),
42
42
  ...(opts.slug ? { slug: opts.slug } : {}),
43
+ ...(opts.attachDatabaseId ? { attachDatabaseId: opts.attachDatabaseId } : {}),
44
+ ...(opts.previewBranchDbId ? { previewBranchDbId: opts.previewBranchDbId } : {}),
43
45
  }),
44
46
  });
45
47
  await uploadAll(dir, created.upload.uploads);
package/package.json CHANGED
@@ -1,23 +1,38 @@
1
- {
2
- "name": "shiply-cli",
3
- "version": "0.7.1",
4
- "description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": { "shiply": "dist/index.js" },
8
- "files": ["dist", "skill", "README.md"],
9
- "scripts": {
10
- "build": "tsc -p tsconfig.build.json",
11
- "prepublishOnly": "pnpm build",
12
- "test": "vitest run",
13
- "typecheck": "tsc --noEmit"
14
- },
15
- "engines": { "node": ">=18" },
16
- "keywords": ["shiply", "hosting", "static", "deploy", "agents", "publish"],
17
- "homepage": "https://shiply.now",
18
- "devDependencies": {
19
- "typescript": "^5.8.0",
20
- "vitest": "^3.1.0",
21
- "@types/node": "^22.15.0"
22
- }
23
- }
1
+ {
2
+ "name": "shiply-cli",
3
+ "version": "0.12.0",
4
+ "description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "shiply": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "skill",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "prepublishOnly": "pnpm build",
18
+ "test": "vitest run",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "shiply",
26
+ "hosting",
27
+ "static",
28
+ "deploy",
29
+ "agents",
30
+ "publish"
31
+ ],
32
+ "homepage": "https://shiply.now",
33
+ "devDependencies": {
34
+ "typescript": "^5.8.0",
35
+ "vitest": "^3.1.0",
36
+ "@types/node": "^22.15.0"
37
+ }
38
+ }
package/skill/SKILL.md CHANGED
@@ -1,129 +1,359 @@
1
- ---
2
- name: shiply
3
- description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
4
- ---
5
-
6
- # shiply — instant web hosting for agents
7
-
8
- shiply.now puts files on the web in seconds. No account needed to start:
9
- anonymous sites are live immediately, last 24 hours, and can be claimed into
10
- an account to keep them. The cardinal rule:
11
-
12
- **NEVER create a new site to update an existing one. Always re-publish to the
13
- same site** — otherwise you litter subdomains and lose the user's URL.
14
-
15
- ## Pick your interface (best first)
16
-
17
- ### 1. MCP (native tools)
18
- If the `shiply` MCP server is connected (https://shiply.now/mcp), use
19
- `publish_site`. Every result includes a `toUpdate` field telling you the exact
20
- call for updates — follow it, and a `shareSuggestion` you can relay to the user.
21
- Other tools: `site_status`, `list_sites`, `get_site`, `set_handle`,
22
- `duplicate_site`, `set_variable`, `add_domain`/`list_domains`/`check_domain`/
23
- `remove_domain`, `get_analytics`, `delete_site`.
24
-
25
- Newer capabilities (use when relevant):
26
- - `set_site_access` — password / invite-only protect a site (paid).
27
- - `set_link` — mount another owned **public** site at a path on a host
28
- (host/docs → target).
29
- - `set_profile` / `feature_site` — stand up the user's public portfolio at
30
- <handle>.shiply.now and feature a public site on shiply.now/explore.
31
- - **Drives** (private cloud storage for files/notes/context): `list_drives`,
32
- `create_drive`, `drive_put_file` (driveId "default", utf8/base64, ≤2 MB),
33
- `drive_list_files`, `drive_delete_file`, `publish_from_drive` (snapshot a
34
- drive into a live site). Great for agent memory and assets you don't want on
35
- a public site.
36
- - `export_account` — JSON bundle of the user's data (no secrets).
37
-
38
- ### 2. CLI
39
- ```bash
40
- npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
41
- shiply publish ./dir # live URL + confetti
42
- shiply publish ./dir # run AGAIN after edits → updates the SAME site
43
- shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
44
- shiply login # email code → API key → sites become permanent
45
- ```
46
- The CLI stores each directory's site in `.shiply.json` (slug + update token),
47
- so repeat publishes reuse the URL automatically. `--new-site` opts out.
48
- Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
49
- lines for automation; exit code 0 = ready.
50
-
51
- ### 3. Raw HTTP (no installs)
52
- ```
53
- 1. POST https://shiply.now/api/v1/publish
54
- {"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
55
- (+ "Authorization: Bearer shp_…" for permanent owned sites)
56
- 2. PUT each file's bytes to response upload.uploads[].url
57
- 3. POST upload.finalizeUrl with {"versionId":"..."}
58
- ```
59
- **Updates:** anonymousinclude `"claimToken":"..."` (returned ONCE by the
60
- first publish save it); owned include `"slug":"..."`. Hashes make updates
61
- cheap: unchanged files are skipped server-side.
62
-
63
- ## The lifecycle to explain to users
64
- - Anonymous site: live instantly, expires in 24 h. Give the user the
65
- `claimUrl` claiming keeps it forever on a free account.
66
- - API key (`shiply login` or POST /api/auth/agent/request-code →
67
- verify-code): publishes are permanent and manageable.
68
- - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
69
- storage, analytics: https://shiply.now/dashboard/plan
70
-
71
- ## Power features (Bearer key)
72
- - **Custom domains**: POST /api/v1/domains {"hostname","slug"} tell the
73
- user to CNAME the hostname to `cname.shiply.now` cert auto-issues; poll
74
- GET /api/v1/domains/{id}/check until `ready:true`.
75
- - **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
76
- check endpoint confirms certificate + serving before telling the user
77
- it's done.
78
- - **Variables**: encrypted per-user KV for API keys the user's sites need
79
- (GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
80
- be connected from the dashboard and lands here as SUPABASE_URL +
81
- SUPABASE_ANON_KEY.
82
- - **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
83
- finalized version live instantly.
84
- - **SPA**: pass `"spaMode":true` so deep links serve index.html.
85
-
86
- ## Proxy routes AI/API calls WITHOUT exposing keys
87
- NEVER embed API keys in published HTML/JS. Instead: store the key as a
88
- Variable (PUT /api/v1/variables), then include `.shiply/proxy.json` in the
89
- published files declaring routes, e.g. {"proxies":{"/api/chat":{"upstream":
90
- "https://openrouter.ai/api/v1/chat/completions","method":"POST","headers":
91
- {"Authorization":"Bearer ${OPENROUTER_API_KEY}"}}}}. The page then calls the
92
- relative path (fetch('/api/chat')) and shiply injects the secret server-side.
93
- Prefix routes "/api/db/*" forward subpaths (great for Supabase REST).
94
- Owned sites only; https public upstreams only. Docs: /docs/proxy-routes
95
-
96
- ## Site Dataforms/waitlists/guestbooks with ZERO backend
97
- When the user wants to collect anything from visitors (waitlist, contact
98
- form, RSVP, guestbook): include `.shiply/data.json` in the published files,
99
- e.g. {"collections":{"signups":{"fields":{"email":{"type":"email",
100
- "required":true}},"access":{"read":"owner","insert":"public"}}}}, then have
101
- the page POST JSON to the relative path /.shiply/data/signups. Records are
102
- validated server-side; the owner reads them in the dashboard (Data section,
103
- CSV export) or GET /api/v1/publishes/<slug>/data/<collection> with a Bearer
104
- key. Owned sites only. Docs: /docs/site-data
105
-
106
- ## Make a site private (paid plans)
107
- To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
108
- with {"mode":"password","password":"..."} or {"mode":"restricted",
109
- "allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
110
- MCP tool set_site_access does the same. Enforced before any content is served;
111
- changing settings signs current visitors out. Docs: /docs/access-control
112
-
113
- ## Full React / Vue / Svelte / Astro apps — YES, they work
114
- shiply hosts complete framework apps. Publish the BUILD OUTPUT, never the
115
- source: run the build, then publish the output dir with SPA mode for
116
- client-routed apps. The CLI auto-detects source dirs and tells you the
117
- right command.
118
- - Vite/React/Vue/Svelte: `npm run build` then `shiply publish dist --spa`
119
- - Create React App: `npm run build` then `shiply publish build --spa`
120
- - Next.js: set `output: "export"` in next.config, `npm run build`, then
121
- `shiply publish out` (no --spa; export emits real HTML per route)
122
- - Astro/static generators: `shiply publish dist`
123
- Use the CLI (not inline MCP files) for builds — hashed bundles often exceed
124
- the 50-file inline cap, and the CLI hash-skips unchanged chunks on updates.
125
-
126
- ## Limits & references
127
- ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
128
- Machine guide: https://shiply.now/llms.txt · OpenAPI:
129
- https://shiply.now/openapi.json · Docs: https://shiply.now/docs
1
+ ---
2
+ name: shiply
3
+ description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
4
+ ---
5
+
6
+ # shiply — instant web hosting for agents
7
+
8
+ shiply.now puts files on the web in seconds. No account needed to start:
9
+ anonymous sites are live immediately, last 24 hours, and can be claimed into
10
+ an account to keep them. The cardinal rule:
11
+
12
+ **NEVER create a new site to update an existing one. Always re-publish to the
13
+ same site** — otherwise you litter subdomains and lose the user's URL.
14
+
15
+ ## First publish on a new machine: ask for a permanent key in one step
16
+
17
+ On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
18
+ (e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
19
+ object alongside the usual `claimUrl`:
20
+
21
+ ```
22
+ "deviceAuth": {
23
+ "user_code": "ABCD-EFGH",
24
+ "device_code": "<long opaque token, keep private>",
25
+ "verification_url": "https://shiply.now/auth/ABCD-EFGH",
26
+ "poll_url": "https://shiply.now/api/v1/auth/device/poll",
27
+ "expires_in": 900,
28
+ "interval": 2
29
+ }
30
+ ```
31
+
32
+ Tell the user once, in plain language:
33
+
34
+ > Your site is live at <siteUrl>. To keep it forever AND let me publish more
35
+ > without re-pasting, open <verification_url> and click Allow.
36
+
37
+ Then immediately poll `verification_url`'s sibling endpoint until the user
38
+ acts:
39
+
40
+ ```
41
+ POST https://shiply.now/api/v1/auth/device/poll
42
+ {"device_code": "<the device_code from the response>"}
43
+ ```
44
+
45
+ The response is one of `{status:"pending"}` (keep polling at `interval`s),
46
+ `{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (save the key as
47
+ `SHIPLY_API_KEY`, the site is now permanent Allow does both jobs in one
48
+ click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
49
+ the announced `expires_in` and respect `interval` don't tight-loop.
50
+
51
+ After approval, every future publish uses `Authorization: Bearer <api_key>`
52
+ and the user's dashboard shows their sites without any further claiming.
53
+
54
+ ## When the user pastes you something shiply-shaped, do this
55
+
56
+ These are the patterns you will see. Match the input, run the action — do not
57
+ ask the user to repeat themselves or explain.
58
+
59
+ - **Code like `SHIPLY-XXXXXXXX`** it's a pairing code from
60
+ `shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
61
+ site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
62
+ directory you originally published from (it reads `.shiply.json`). No CLI?
63
+ POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
64
+ `{"siteId":"...","claimToken":"..."}` taken from your earlier publish
65
+ response or `.shiply.json`. The user's browser auto-redirects on success;
66
+ tell them to switch back to it.
67
+ - **URL like `https://shiply.now/claim/<slug>?token=...`** one-click claim
68
+ link. Tell the user to open it in the browser they are signed in to —
69
+ do NOT call any API yourself.
70
+ - **User says "claim my site" / "save it forever" with no code or URL** →
71
+ give them the `claimUrl` from your last publish response. If you no longer
72
+ have that response but `.shiply.json` is in the publish dir, reconstruct it
73
+ as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
74
+ Either way, also suggest `shiply login` so future publishes mint to their
75
+ account directly that's the long-term fix and how the dashboard works.
76
+ - **User says "I have an API key now, use it from now on"** → save it as
77
+ `SHIPLY_API_KEY` in your environment / config and send
78
+ `Authorization: Bearer <key>` on every future publish. All future sites
79
+ are then permanent and appear in their dashboard automatically.
80
+ - **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** no
81
+ action from you; they're on the device-flow consent screen. Keep polling
82
+ `poll_url` (see "First publish on a new machine" above). The poll response
83
+ will flip from `pending` to `approved` (with `api_key`) the moment they
84
+ click Allow.
85
+
86
+ ## Pick your interface (best first)
87
+
88
+ ### 1. MCP (native tools)
89
+ If the `shiply` MCP server is connected (https://shiply.now/mcp), use
90
+ `publish_site`. Every result includes a `toUpdate` field telling you the exact
91
+ call for updates follow it, and a `shareSuggestion` you can relay to the user.
92
+ Other tools: `site_status`, `list_sites`, `get_site`, `set_handle`,
93
+ `duplicate_site`, `set_variable`, `get_analytics`, `delete_site`, `whoami`,
94
+ `add_custom_domain`, `connect_provider`, `add_subdomain`, `check_custom_domain`,
95
+ `sync_dns`, `list_custom_domains`, `remove_custom_domain`
96
+ (auto-DNS via OAuth provider connections see **Custom domains** section below).
97
+
98
+ Newer capabilities (use when relevant):
99
+ - `set_site_access` — password / invite-only protect a site (paid).
100
+ - `set_link` — mount another owned **public** site at a path on a host
101
+ (host/docs target).
102
+ - `set_profile` / `feature_site` stand up the user's public portfolio at
103
+ <handle>.shiply.now and feature a public site on shiply.now/explore.
104
+ - **Drives** (private cloud storage for files/notes/context): `list_drives`,
105
+ `create_drive`, `drive_put_file` (driveId "default", utf8/base64, ≤2 MB),
106
+ `drive_list_files`, `drive_delete_file`, `publish_from_drive` (snapshot a
107
+ drive into a live site). Great for agent memory and assets you don't want on
108
+ a public site.
109
+ - `export_account` JSON bundle of the user's data (no secrets).
110
+
111
+ ### 2. CLI
112
+ ```bash
113
+ npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
114
+ shiply publish ./dir # live URL + confetti
115
+ shiply publish ./dir # run AGAIN after edits updates the SAME site
116
+ shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
117
+ shiply login # email code → API key → sites become permanent
118
+ shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
119
+ # (uses .shiply.json from CWD; pairs agent session to user's browser)
120
+ ```
121
+ The CLI stores each directory's site in `.shiply.json` (slug + update token),
122
+ so repeat publishes reuse the URL automatically. `--new-site` opts out.
123
+ Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
124
+ lines for automation; exit code 0 = ready.
125
+
126
+ ### 3. Raw HTTP (no installs)
127
+ ```
128
+ 1. POST https://shiply.now/api/v1/publish
129
+ {"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
130
+ (+ "Authorization: Bearer shp_…" for permanent owned sites)
131
+ 2. PUT each file's bytes to response upload.uploads[].url
132
+ 3. POST upload.finalizeUrl with {"versionId":"..."}
133
+ ```
134
+ **Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
135
+ first publish — save it); owned → include `"slug":"..."`. Hashes make updates
136
+ cheap: unchanged files are skipped server-side.
137
+
138
+ ## The lifecycle to explain to users
139
+ - Anonymous site: live instantly, expires in 24 h. Give the user the
140
+ `claimUrl` — claiming keeps it forever on a free account.
141
+ - API key (`shiply login` or POST /api/auth/agent/request-code →
142
+ verify-code): publishes are permanent and manageable.
143
+ - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
144
+ storage, analytics: https://shiply.now/dashboard/plan
145
+
146
+ ## Custom domains
147
+
148
+ Serve a site at `www.yourdomain.com` instead of a `*.shiply.now` URL. Requires a paid plan and a Bearer API key.
149
+
150
+ ### CLI commands
151
+
152
+ ```bash
153
+ shiply domain add example.com # register a root domain (detects provider)
154
+ shiply domain connect example.com # one-click OAuth DNS setup (Cloudflare, GoDaddy, IONOS, etc.)
155
+ shiply domain sub add www.example.com --site my-site # map a subdomain → site
156
+ shiply domain ls # list all domains + subdomains + status
157
+ shiply domain sync example.com # re-sync DNS records with a connected provider
158
+ ```
159
+
160
+ For one-click connect (`shiply domain connect`), the CLI prints an authorization URL — the user must open it in a browser and approve the OAuth grant before DNS is written.
161
+
162
+ For unsupported providers, `shiply domain add` returns the CNAME record to add manually:
163
+ `www CNAME cname.shiply.now`
164
+
165
+ Check status after connecting:
166
+ ```bash
167
+ shiply status www.example.com --wait # polls until SSL_READY + SITE_READY; exit 0 when live
168
+ ```
169
+
170
+ ### MCP tools (same operations via agent tools)
171
+
172
+ | Tool | Purpose |
173
+ |------|---------|
174
+ | `add_custom_domain` | Add a root domain and detect the provider |
175
+ | `connect_provider` | Start OAuth → returns `{ url }` (show to user to authorize) |
176
+ | `add_subdomain` | Map a subdomain to a site slug |
177
+ | `check_custom_domain` | Live CNAME + SSL + readiness probe |
178
+ | `sync_dns` | Re-push DNS records to a connected provider |
179
+ | `list_custom_domains` | List all domains and their subdomain mappings |
180
+ | `remove_custom_domain` | Remove a domain and all its mappings |
181
+
182
+ ### REST endpoints (`/api/v1/custom-domains`)
183
+
184
+ ```
185
+ GET /api/v1/custom-domains list domains
186
+ POST /api/v1/custom-domains add domain { domain }
187
+ DELETE /api/v1/custom-domains/{domain} remove domain
188
+ POST /api/v1/custom-domains/{domain}/subdomains add subdomain { subdomain, slug }
189
+ POST /api/v1/custom-domains/{domain}/connect start OAuth → { url }
190
+ POST /api/v1/custom-domains/{domain}/sync-dns re-sync DNS
191
+ POST /api/v1/custom-domains/{domain}/check live status
192
+ ```
193
+
194
+ All require `Authorization: Bearer shp_…`.
195
+
196
+ ## Power features (Bearer key)
197
+ - **Custom domains**: see the **Custom domains** section above.
198
+ REST: `POST /api/v1/custom-domains {"domain"}` → add domain + detect provider.
199
+ Then `POST /api/v1/custom-domains/{domain}/connect` → OAuth one-click (show URL to user)
200
+ or add the CNAME manually. Map subdomains: `POST /api/v1/custom-domains/{domain}/subdomains {"subdomain","slug"}`.
201
+ Poll `POST /api/v1/custom-domains/{domain}/check` until `ready:true`.
202
+ - **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
203
+ check endpoint — confirms certificate + serving before telling the user
204
+ it's done.
205
+ - **Variables**: encrypted per-user KV for API keys the user's sites need
206
+ (GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
207
+ be connected from the dashboard and lands here as SUPABASE_URL +
208
+ SUPABASE_ANON_KEY.
209
+ - **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
210
+ finalized version live instantly.
211
+ - **SPA**: pass `"spaMode":true` so deep links serve index.html.
212
+
213
+ ## Proxy routes — AI/API calls WITHOUT exposing keys
214
+ NEVER embed API keys in published HTML/JS. Instead: store the key as a
215
+ Variable (PUT /api/v1/variables), then include `.shiply/proxy.json` in the
216
+ published files declaring routes, e.g. {"proxies":{"/api/chat":{"upstream":
217
+ "https://openrouter.ai/api/v1/chat/completions","method":"POST","headers":
218
+ {"Authorization":"Bearer ${OPENROUTER_API_KEY}"}}}}. The page then calls the
219
+ relative path (fetch('/api/chat')) and shiply injects the secret server-side.
220
+ Prefix routes "/api/db/*" forward subpaths (great for Supabase REST).
221
+ Owned sites only; https public upstreams only. Docs: /docs/proxy-routes
222
+
223
+ ## Site Data — forms/waitlists/guestbooks with ZERO backend
224
+ When the user wants to collect anything from visitors (waitlist, contact
225
+ form, RSVP, guestbook): include `.shiply/data.json` in the published files,
226
+ e.g. {"collections":{"signups":{"fields":{"email":{"type":"email",
227
+ "required":true}},"access":{"read":"owner","insert":"public"}}}}, then have
228
+ the page POST JSON to the relative path /.shiply/data/signups. Records are
229
+ validated server-side; the owner reads them in the dashboard (Data section,
230
+ CSV export) or GET /api/v1/publishes/<slug>/data/<collection> with a Bearer
231
+ key. Owned sites only. Docs: /docs/site-data
232
+
233
+ ## SQL databases (Cloudflare D1 + Neon Postgres)
234
+ Two engines, same CLI/REST/MCP surface. **D1 (SQLite at the edge)** is
235
+ free on every plan and queryable from the browser via a built-in fetch
236
+ shim — no API key in the page. **Neon Postgres** is a developer-plan
237
+ add-on with copy-on-write branching; the connection URI is encrypted at
238
+ rest and surfaced to the site's Worker as `env.DATABASE_URL`.
239
+
240
+ ### D1 (default)
241
+ ```bash
242
+ shiply db create app # provision a D1 (binding APP_DB)
243
+ shiply db ls # list (name, provider, size, attached site)
244
+ shiply db sql app "SELECT 1" # one-shot query against your account
245
+ shiply db sql app "SELECT * FROM t WHERE id=?1" --params '[42]'
246
+ shiply db migrate app ./migrations # apply every *.sql in dir, sorted
247
+ shiply db attach app --site my-site # bind an existing DB to another site
248
+ shiply db delete app --yes # drop the DB and every row
249
+ ```
250
+
251
+ Run `shiply db create` inside a publish directory: it records the new
252
+ `databaseId` into `.shiply.json` so the next `shiply publish` auto-attaches
253
+ it. Site code then calls the shim:
254
+
255
+ ```js
256
+ fetch('/_shiply/db/APP_DB/query', {
257
+ method: 'POST',
258
+ headers: { 'content-type': 'application/json' },
259
+ body: JSON.stringify({ sql: 'SELECT * FROM posts', params: [] }),
260
+ }).then(r => r.json()) // → { results: [...], meta: {rows_read, rows_written, duration} }
261
+ ```
262
+
263
+ **Public by default**: the shim has no visitor auth, so anyone who can fetch
264
+ the site can run any SQL. Until a private-mode gate ships, recommend it
265
+ strictly as a SELECT-only public data layer. Reads always work; writes hit
266
+ 402 once the DB exceeds the plan storage cap (Founder Special 100 MB,
267
+ Hobby 1 GB, Developer 10 GB).
268
+
269
+ ### Neon Postgres (developer plan)
270
+ Pass `--postgres` on create. One isolated Neon project per database;
271
+ binding defaults to `DATABASE_URL`. Free / Hobby get `402
272
+ payment_required` — point the user at https://shiply.now/dashboard/plan.
273
+
274
+ ```bash
275
+ shiply db create app --postgres # provision Neon (~5–10s)
276
+ shiply db branch app dev # copy-on-write branch off main
277
+ shiply db branches app # list branches
278
+ shiply db delete-branch dev --yes # drop branch + endpoint
279
+ shiply db merge dev # NO-OP — prints pg_dump tip (see below)
280
+ ```
281
+
282
+ After the next `shiply publish` the serving Worker receives
283
+ `env.DATABASE_URL`. Minimal example:
284
+
285
+ ```js
286
+ import { neon } from '@neondatabase/serverless'
287
+ const sql = neon(process.env.DATABASE_URL)
288
+ const rows = await sql`SELECT now()`
289
+ ```
290
+
291
+ Anything that speaks Postgres works: `pg`, `postgres`, drizzle, kysely,
292
+ prisma (with the Neon adapter).
293
+
294
+ **Branching is the headline feature.** Branches are cheap, instant,
295
+ copy-on-write forks of `main`. Bind a publish to a specific branch with
296
+ `shiply publish --preview-branch=<branchDbId>` (use the `site_databases`
297
+ row id printed by `shiply db branch`, NOT the raw Neon branch id) —
298
+ great for preview deploys and safe schema rollouts.
299
+
300
+ **`shiply db merge` is intentionally a no-op.** Neon has no server-side
301
+ merge. To promote branch data into the parent:
302
+ `pg_dump "$BRANCH_DATABASE_URL" | psql "$PARENT_DATABASE_URL"`.
303
+ Run schema migrations against both branches to keep them aligned.
304
+
305
+ **Not yet supported on Neon**: branch-of-branch (branches off `main`
306
+ only), auto preview-branch on every publish, cross-region projects.
307
+
308
+ ### REST + MCP (provider-agnostic)
309
+ REST (Bearer `shp_…`): `POST /api/v1/databases` (pass
310
+ `"provider":"neon"` for Postgres) to create, `POST
311
+ /api/v1/databases/{id}/query` to query (works for D1 and Neon), `POST
312
+ .../attach` to bind, `DELETE` to drop. Neon-only:
313
+ `POST/GET/DELETE /api/v1/databases/{id}/branches[/{branchId}]` (400 on
314
+ D1 rows). Per-DB MCP server (so an agent can be scoped to one DB):
315
+ `https://shiply.now/api/mcp/db/<id>/sse` — tools: `db_query`,
316
+ `db_list_tables`, `db_schema`. The same three tools work against both
317
+ providers; `db_schema` introspects `sqlite_master` (D1) or
318
+ `information_schema` (Neon). Full docs:
319
+ https://shiply.now/docs/databases
320
+
321
+ ## Make a site private (paid plans)
322
+ To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
323
+ with {"mode":"password","password":"..."} or {"mode":"restricted",
324
+ "allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
325
+ MCP tool set_site_access does the same. Enforced before any content is served;
326
+ changing settings signs current visitors out. Docs: /docs/access-control
327
+
328
+ ## Any static site works — frameworks + SSGs auto-detected
329
+ shiply hosts any static site. Publish the BUILD OUTPUT, never the source:
330
+ run the build, then publish the output dir with SPA mode for client-routed
331
+ apps. The CLI auto-detects 16+ frameworks and tells you the right command.
332
+ Run `shiply detect` to preview what it found without uploading.
333
+ - Vite (React/Vue/Svelte/Solid/Qwik): `npm run build` then `shiply publish dist --spa`
334
+ - Create React App: `npm run build` then `shiply publish build --spa`
335
+ - Next.js: set `output: "export"` in next.config, `npm run build`, then
336
+ `shiply publish out` (no --spa; export emits real HTML per route)
337
+ - Astro: `npm run build` then `shiply publish dist`
338
+ - SvelteKit (adapter-static): `npm run build` then `shiply publish build --spa`
339
+ - Nuxt: `npx nuxt generate` then `shiply publish .output/public`
340
+ - Remix (client build): `npm run build` then `shiply publish build/client --spa`
341
+ - Docusaurus: `npm run build` then `shiply publish build`
342
+ - Hugo: `hugo` then `shiply publish public`
343
+ - Jekyll: `bundle exec jekyll build` then `shiply publish _site`
344
+ - Eleventy: `npx @11ty/eleventy` then `shiply publish _site`
345
+ - MkDocs: `mkdocs build` then `shiply publish site`
346
+ - Hexo: `npx hexo generate` then `shiply publish public`
347
+ - SolidStart (static): `npm run build` then `shiply publish dist/public`
348
+ - Qwik City (static adapter): `npm run build` then `shiply publish dist`
349
+ - Anything else with a built `dist/`/`build/`/`out/`/`_site/`/`public/`/
350
+ `.output/public/` folder: generic fallback picks it up.
351
+ - Plain HTML: `shiply publish .` — no build step.
352
+ Override auto-detection with `--framework=<name>`. Use the CLI (not inline
353
+ MCP files) for builds — hashed bundles often exceed the 50-file inline cap,
354
+ and the CLI hash-skips unchanged chunks on updates.
355
+
356
+ ## Limits & references
357
+ ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
358
+ Machine guide: https://shiply.now/llms.txt · OpenAPI:
359
+ https://shiply.now/openapi.json · Docs: https://shiply.now/docs