shiply.now

Databases

Per-site SQL databases — Cloudflare D1 (SQLite) on every plan, Neon Postgres on the developer plan with copy-on-write branching

shiply gives every account a real SQL database. Pick the engine that fits the job:

  • Cloudflare D1 (SQLite at the edge) — default. Free on every plan. Great for catalogs, configuration, simple write-light apps.
  • Neon Postgres — opt in with --postgres. Requires the developer plan. One isolated Neon project per database, copy-on-write branches off main, and env.DATABASE_URL auto-injected into your serving Worker.

The CLI, REST API, and MCP contracts are provider-agnostic — the same commands work against either engine. The differences are documented inline below.

CLI

shiply db create app                 # provision a D1 (binding SITE_DB by default)
shiply db create app --postgres      # provision a Neon Postgres (binding DATABASE_URL) — developer plan
shiply db ls                         # list databases (name, provider, size, attached site)
shiply db sql app "SELECT 1"         # one-shot query (uses your API key) — works on D1 + Neon
shiply db sql app "SELECT * FROM users WHERE id = ?1" --params '[42]'
shiply db migrate app ./migrations   # apply every *.sql file in dir, sorted
shiply db attach app --site my-site  # attach an existing DB to a different site
shiply db delete app --yes           # drop the DB and every row in it

# Neon-only — branching off main:
shiply db branch app dev             # copy-on-write Neon branch (cheap, fast)
shiply db branches app               # list branches of a Neon database
shiply db delete-branch dev --yes    # drop a Neon branch (and its compute endpoint)
shiply db merge dev                  # no-op alias — prints the pg_dump migration tip

A few things to know:

  • Auto-attach on publish. Running shiply db create inside a directory that already has a .shiply.json records the new databaseId there, so the next shiply publish binds it without extra steps. Works for D1 and Neon alike.
  • <name-or-id> accepts either. A UUID is used directly; anything else is resolved to your DB by name. Two DBs can never share a name per account.
  • Bindings default to SITE_DB for D1, DATABASE_URL for Neon. Override with --binding (the dashboard auto-derives <NAME>_DB from the database name when you create from the UI). The binding name is the path segment your site queries for D1 (see Querying from a published site); for Neon it's the process.env.<name> your code reads.

Querying from a published site (/_shiply/db/<binding>/query)

When a database is attached to a site, shiply mounts a reserved-namespace fetch shim at the site root. Your published HTML/JS calls a relative URL — no API key, no domain config:

const res = await fetch('/_shiply/db/SITE_DB/query', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    sql: 'SELECT id, title FROM posts WHERE slug = ?1',
    params: [slug],
  }),
})
const { results } = await res.json()

The response shape mirrors D1's: { results: [...rows], meta: { rows_read, rows_written, duration } }.

Public by default. The shim runs as the site owner unconditionally — there is no visitor JWT to authenticate. Anyone who can fetch your site can run any SQL against the bound database. Until a private-mode gate ships, treat the shim as a SELECT-only data layer: read public catalogs, not user PII or write paths. The plan storage cap and size guard still apply, so a hostile visitor can't blow past quota.

Postgres (Neon) — developer plan

shiply db create --postgres <name> provisions a fully isolated Neon Postgres project under shiply's organisation — one Neon project per database — and binds it to your site on the next shiply publish. Your serving Worker receives the connection string as env.DATABASE_URL; the URI itself is AES-256-GCM encrypted at rest (same secretbox envelope used for account variables) and only decrypted server-side when the bindings ship to the Worker.

shiply db create app --postgres        # ~5–10s — Neon project creation
shiply publish ./dist                  # next publish injects env.DATABASE_URL

A minimal serverless query:

import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL)
const rows = await sql`SELECT now()`

Anything that speaks Postgres works — pg, postgres, drizzle, kysely, prisma (with the Neon adapter). Connections are pooled and HTTPS-only.

Branching

Branches are Neon's headline feature: instant, copy-on-write forks of your main database. Use them for previews, schema migrations, or throwaway experiments without copying data.

shiply db branch app dev               # branch off main → 'dev'
shiply db branches app                 # list branches
shiply db delete-branch dev --yes      # drop branch + endpoint
shiply db merge dev                    # NOOP — see "no auto-merge" below

Each branch is its own row in your databases list, with its own DATABASE_URL and its own compute endpoint. Attach a branch to a site the same way you attach the parent — by including its id in .shiply.json (the branch command prints it).

No auto-merge. shiply db merge is a deliberate no-op alias — Neon branches don't have a server-side merge primitive. To promote a branch's data into the parent:

pg_dump  "$BRANCH_DATABASE_URL"  | psql "$PARENT_DATABASE_URL"

To keep schemas aligned, run your migrations against both branches. The merge command exists so agents reaching for it get pointed at this pattern instead of failing with "unknown command".

Preview-branch publishes

To bind a specific publish version to a branch instead of main — useful for preview deploys, A/B tests, or safe schema rollouts — pass the branch row id (the site_databases.id, NOT the Neon branch id) to shiply publish:

shiply publish ./dist --preview-branch=<branchDbId>

The version that goes live binds env.DATABASE_URL to the branch's URI. Subsequent publishes without the flag fall back to the parent. If the id is foreign or unowned, the publish silently uses the parent URI — the flag is best-effort, not a hard gate.

MCP — same tools, Postgres dialect

The per-database MCP server (/api/mcp/db/<id>/(sse|mcp)) works identically for Neon: db_query, db_list_tables, and db_schema all dispatch to Postgres under the hood. Schema introspection uses information_schema instead of D1's sqlite_master, so an agent sees proper Postgres types (integer, timestamp with time zone, etc.).

Plan + limits

PlanNeon accessBranches per databaseStorage per branch
Founder Special / Hobby
DeveloperNeon's default (10)Neon's default (50 GB)

Trying to create a Postgres database on Founder Special / Hobby returns 402 payment_required with a pointer to the plan page. Compute hours are re-sold at 2× Neon's underlying cost, included in the developer plan up to a generous monthly cap; future overage billing will surface in the dashboard before it bites.

What's not yet supported

  • Branch-of-branch. Branches are off main only. The Neon API allows nested branches; we don't expose them in v1.
  • Auto preview-branch on every publish. Opt in per-publish with --preview-branch=<branchDbId>. Auto-branching every preview is too noisy and expensive to make default.
  • Cross-region. All Neon projects land in shiply's default region.

REST API (Authorization: Bearer shp_…)

Method & pathPurpose
GET /api/v1/databaseslist your databases (D1 + Neon)
POST /api/v1/databases{name, siteSlug?, binding?, provider?} → provision; provider defaults to "d1", pass "neon" for Postgres
GET /api/v1/databases/{id}one database (size, attached site, provider)
DELETE /api/v1/databases/{id}drop the database
POST /api/v1/databases/{id}/query{sql, params?}{results, meta} (works for both providers)
POST /api/v1/databases/{id}/attach{siteSlug} → bind an existing DB to a site
POST /api/v1/databases/{id}/branches{name} → create a Neon branch (400 on D1)
GET /api/v1/databases/{id}/brancheslist branches of a Neon database
DELETE /api/v1/databases/{id}/branches/{branchId}drop a Neon branch + endpoint

The query route is the same code path as the per-site shim, but authenticated with your API key — use it for migrations, exports, anything you don't want to run unauthenticated.

MCP — per-database server

Every database gets its own MCP endpoint so an agent can be scoped to a single DB without seeing the rest of your account:

https://shiply.now/api/mcp/db/<databaseId>/sse
https://shiply.now/api/mcp/db/<databaseId>/mcp     # streamable-http

Authenticate with Authorization: Bearer shp_…. Three tools are registered:

  • db_query{sql, params?}{results, meta}
  • db_list_tables{}{tables}
  • db_schema{table}{columns}

This is the same surface as the global shiply MCP, narrowed to one database — useful for handing an agent a SQL-only context. The same three tools work against D1 (SQLite) and Neon (Postgres) — the dispatcher picks the right backend per database id, and db_schema / db_list_tables introspect sqlite_master or information_schema accordingly.

Plan limits

PlanD1 databasesD1 size per DBNeon Postgres
Founder Special (free)1100 MB
Hobby51 GB
Developerunlimited10 GB✓ (with branching)

D1 size is sampled every 15 minutes by a background poller and stored as meta.lastSizeBytes. Writes are rejected with 402 payment_required once a D1 database is over its plan cap; reads still work so you can export data or delete rows to get back under quota. The 10 GB ceiling on Developer is D1's per-database limit — split larger datasets across multiple databases.

Neon storage is billed against shiply's developer plan compute-hour allowance (re-sold at 2× Neon's underlying rate). See the Postgres (Neon) section above for the branch + compute trade-offs.

D1 caveats (SQLite dialect)

  • SQLite syntax. INTEGER PRIMARY KEY AUTOINCREMENT, TEXT, REAL, BLOB, BOOLEAN (stored as INTEGER). No SERIAL, no RETURNING * on every statement (D1 supports RETURNING on INSERT/UPDATE/DELETE).
  • No cross-database joins. Each D1 is isolated. If you need a relation spanning datasets, keep them in one DB.
  • No PRAGMA journal_mode or other server-tuning PRAGMAs — D1 manages those for you.
  • One statement per query call in the shim path. Use shiply db migrate for multi-statement scripts; it sends each file as one request.

Errors

Statuserror.codeFix
402payment_requiredDB count or size over plan cap — delete data or upgrade
403forbiddennot your database (or a write to a suspended one)
404not_foundbad <id>, or shim path with no DB bound at that binding
400invalid_requestbad SQL, bad params JSON, or a malformed name