Data & backend

Any page gets a zero-config backend by adding one script tag, with no keys to manage.

<script src="/quick.js"></script>

That gives you quick.db (a document store), quick.files (uploads), quick.identity() (the signed-in visitor), quick.realtime(), and quick.cast() (turn the page into a screen you control from your phone).

Store and read documents

const tasks = quick.db.collection("tasks");

await tasks.create({ title: "Ship it", done: false });
const all = await tasks.list();
await tasks.update(all[0].id, { done: true });
await tasks.remove(all[0].id);

// live updates over websockets
tasks.subscribe({ onCreate: t => console.log("new task", t) });

Filter, sort & page

list() takes an optional query so you don't have to pull the whole collection:

// open tasks, newest first, 20 at a time
await tasks.list({ where: { done: false }, order: "-created_at", limit: 20 });

// operators: gt / gte / lt / lte / ne / in
await tasks.list({ where: { votes: { gte: 3 }, status: { in: ["open", "wip"] } } });

// page with limit + offset
await tasks.list({ order: "title", limit: 20, offset: 40 });

Filters match top-level fields (max 500 rows per call) and map straight to the HTTP API as ?where=<json>&order=-created_at&limit=20. Values are always parameters — a query can never inject SQL.

Who can read and write

A collection is owned by the page that creates it. By default it's readable by your page's audience and writable only by you — so a public page can show data without letting visitors change it. Set per-operation rules when you declare it:

// everyone reads; only you write (the default, written out)
quick.db.collection("posts", { read: "audience", write: "owner" });

// signed-in visitors comment; each edits only their own; you can moderate
quick.db.collection("comments", {
  read: "audience", create: "users",
  update: "author", delete: ["author", "owner"],
});

Rules name principalsowner · audience · author · users · workspace · public (write = create + update + delete). Same-page access is free; reaching another page's collection needs a paid plan. Full guide: Permissions.

Sample data for remixes

When someone remixes your app, their copy starts with an empty database. Mark a collection as sample data and its documents are seeded into every remix, so the copy works on first run:

// example cards travel with a remix; everything else starts fresh
quick.db.collection("cards", { seed: true });

Only the owner page can set this, and only your own app's data is ever copied (up to 500 records per collection). Set { seed: false } to stop seeding it.

Cast to a screen

Put a Quickish page on a TV, projector, or smart display and push content to it from your phone — a link, an announcement, or an image. One call turns the page into a cast target:

<script src="/quick.js"></script>
<script>quick.cast()</script>

Open that page on the big screen and it shows a short pairing code and a link. On your phone, open the same page, enter the code, and you get a remote that pushes to the screen in real time. quick.cast() figures out which side it's on: it's the screen by default, and becomes the remote when the URL carries a code (#qkcast=CODE) — so the same page is both.

It's built on quick.realtime(), so there's nothing to provision. The screen and the remote both load the page through the edge, which means casting inherits the page's visibility:

Want your own remote or a custom screen instead of the built-ins? Drive it directly:

// On the screen: render pushes yourself
const screen = quick.cast.receive({
  title: "Lobby display",
  render(msg, mount) {            // msg = { type, ... } from a remote
    if (msg.type === "text") mount.innerHTML = `<h1>${msg.text}</h1>`;
  },
});

// On a phone (a page opened with #qkcast=CODE): push your own messages
const remote = quick.cast.connect("AB12");
remote.send({ type: "url", url: "https://example.com/slides" });

Built-in message types: url · image · text · clear. Codes use an unambiguous alphabet and live only for the session.

QR to connect: the screen shows a code by default. To also show a scannable QR, pass quick.cast({ qr: url => "https://your-qr-image/?data=" + encodeURIComponent(url) }). It's opt-in on purpose, so a private page's URL is never handed to a third party unless you choose one.

Org directory workspaces

Every workspace gets a live, read-only directory of its sites built in. quick.org.sites() (or quick.db.collection("org_sites").list()) returns the org's visible sites with hit counts, kept current as teammates publish, so a directory page never needs updating.

const sites = await quick.org.sites();
// [{ id, name, url, owner, visits, visibility, isHome, updated }, ...]

document.querySelector("#dir").innerHTML = sites
  .map(s => `<a href="${s.url}">${s.name} — ${s.visits} views</a>`)
  .join("");
Ask your AI: “build a dynamic directory of our company's Quickish sites, ranked by views” — it can read this with no setup. Members see internal + public sites; the public sees only public ones.

Remote access data tokens

The same quick.db collections are reachable off-page too — for an AI agent that builds your app, or your own script. In your dashboardSettings → Data tokens, mint a token scoped to one page (read/write or read-only), then call the remote API:

# list documents in a collection
curl -H "Authorization: Bearer $QK_DATA_TOKEN" \
  https://api.quickish.website/_data/db/tasks

# create one
curl -X POST -H "Authorization: Bearer $QK_DATA_TOKEN" \
  -H "Content-Type: application/json" -d '{"title":"Ship it"}' \
  https://api.quickish.website/_data/db/tasks

A data token goes through the exact same access rules as the in-page SDK — it's scoped to its page and can only ever narrow access, never widen it. Revoke it anytime. With the Claude connector you don't even need a token: Claude reads and writes your page's data through built-in tools while it builds the app.