Site Data
Built-in storage for forms, waitlists & guestbooks — no backend needed
Site Data gives every published site a tiny built-in database. Declare typed
collections in a manifest, and your pages can save visitor submissions with a
relative fetch — no server, no external database, no API keys.
Starter manifest
Don't have a .shiply/data.json yet? Scaffold one:
shiply data init # writes ./.shiply/data.json
shiply data init ./my-site # or target a specific directoryThe default manifest declares a single messages collection (name, email,
message), public-insert + owner-read, rate-limited to 10/hour/ip. Edit the
fields to match your form, then publish — Site Data starts working
immediately. init refuses to overwrite an existing .shiply/data.json; move
or delete it first if you want to start over.
Manifest: .shiply/data.json
{
"collections": {
"signups": {
"fields": {
"email": { "type": "email", "required": true },
"name": { "type": "string", "maxLength": 200 }
},
"access": { "read": "owner", "insert": "public" },
"rateLimit": "10/hour/ip"
}
}
}- Field types:
string,number,integer,boolean,email,url,datetime. Strings takemaxLength; numbers takeminimum/maximum. - Access per collection:
readandinsert, each"public"or"owner". Default: public inserts, owner-only reads — right for waitlists. Useread: "public"for guestbook-style pages. - rateLimit:
"N/minute|hour|day/ip"for public inserts (default10/hour/ip).
Submitting from your page
<form id="f">
<input id="email" type="email" required placeholder="you@example.com">
<button>Join waitlist</button>
</form>
<script>
document.getElementById('f').onsubmit = async (e) => {
e.preventDefault()
const res = await fetch('/.shiply/data/signups', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: document.getElementById('email').value }),
})
alert(res.ok ? 'Thanks — you are on the list!' : (await res.json()).message)
}
</script>Records are validated against your field specs server-side; bad submissions
get a 400 with a specific message ("email" must be an email address).
Reading records
- Dashboard: every site page has a Data section — browse collections, delete records, export CSV.
- Owner API (Bearer
shp_key):GET /api/v1/publishes/:slug/data/:collection?limit=50&cursor=...→{ records, nextCursor }·DELETE /api/v1/publishes/:slug/data/:collection/:id - Public reads (only when
read: "public"):GET /.shiply/data/:collectionfrom the site itself.
CLI
The shiply data subcommands wrap the owner API for quick inspection,
backups, and admin from your terminal.
shiply data list <slug> # collections + counts
shiply data query <slug> <collection> [--limit 50] [--cursor <iso>] [--where '<json>']
shiply data insert <slug> <collection> --json '{"email":"a@b.co"}'
shiply data export <slug> <collection> [--out out.ndjson] # streams NDJSON
shiply data wipe-collection <slug> <collection> --yes # destructiveNotes:
- All commands except
data initanddata insertneed a Bearer API key (shiply login).data inserthits the public visitor endpoint — the manifest'saccess.insertdecides whether it's allowed. --whereis a JSON object of equality filters (e.g.'{"email":"a@b.co"}'). Filtering is applied client-side, after server-side paging, so it sees the current page only — pair with--limit/--cursorfor large collections or useexportto scan everything.data exportstreams NDJSON: one record per line, newline-delimited. Use--out <file>to write to disk; without it, the stream goes to stdout — pipe tojqor another file for ad-hoc analysis.data wipe-collectionrequires the explicit--yesflag — there is no interactive prompt, so it's safe to script but unforgiving if mistyped.
Limits
| Limit | Value |
|---|---|
| Collections per site | 10 |
| Fields per collection | 30 |
| Record size | 16 KB |
| Records per collection | 25,000 |
| Site must be owned | anonymous sites get 403 — claim first |
MCP tools
When you mount the shiply MCP server (see Reference), four Site Data tools become available to your agent:
| Tool | Input | Returns |
|---|---|---|
data_list_collections | { slug } | { collections: [{ name, count }] } |
data_query | { slug, collection, limit?, cursor? } | { records, nextCursor } — newest-first, limit ≤ 200 (default 50) |
data_insert | { slug, collection, record } | the visitor endpoint's JSON response — hits POST /.shiply/data/:collection, so access.insert applies |
data_export_collection | { slug, collection, limit? } | { records, truncated } — buffered, limit ≤ 5,000 (default 1,000); truncated: true when the cap was hit |
data_list_collections, data_query, and data_export_collection require
owner auth (the MCP server uses your API key); data_insert follows the same
public-or-owner rules as the visitor POST /.shiply/data/:collection
endpoint.
For exports larger than 5,000 rows, fall back to the streaming CLI:
shiply data export <slug> <collection> --out out.ndjson.