quickish docs
Getting started

What is Quickish

Quickish gives any folder of HTML a live URL. No build step, no server to run, no hosting to set up. You make a page; Quickish puts it online.

If you have an index.html, whether written by hand, exported from a tool, or generated by an AI, Quickish turns it into a real website in seconds. Drop a folder and the links inside it just work. When you want more than a static page, add one script tag and you get a backend: a database, file uploads, who's signed in, live updates, and small server functions, all with no keys to manage.

Three ways to publish

Web

Sign in and drop a folder, files, a .zip, or a single .html onto your dashboard.

CLI

One command from your terminal. Great for developers and coding agents.

AI popular

Connect Quickish to your assistant once, then just say “publish this.”

What you can build

Start with a one-page site and grow into a real app, all on the same URL:

  • A landing page, a résumé, a wedding invite, a link-in-bio, published in seconds.
  • A guestbook, a poll, a kanban board, a sprint-retro, using quick.db for storage and quick.realtime for live updates.
  • A leaderboard or anything with real relationships, using quick.sql.
  • Anything that needs a secret key or trusted logic like a checkout or a contact form, using server functions.

How to read these docs

Getting started is the short path from nothing to a published page. Guides go a step deeper on each capability, in plain language. Workspace covers shared company accounts. The API reference documents every quick.* call precisely, for when you want the exact signature.

New here? Read Publish your first page next. It takes about two minutes end to end.
Getting started

Publish your first page

Get a live URL three ways: from the web, from your terminal, or by asking an AI assistant.

From the web

Sign in with Google, then on your dashboard drop a folder (or a .zip or a single .html) onto the drop area. It's live at your space URL right away. Keep an index.html at the root so the page has a homepage.

From the terminal

Install the CLI once, sign in, and publish the current folder:

$ npm i -g quickish
$ quickish login
$ quickish            # publish the current folder

Publish a named sub-page by passing a name: quickish ./site deck serves at /deck/.

Built a React / Vite / Astro app? Quickish hosts the build output, not the source. Run quickish --build to build on your machine and publish the result, or build it yourself and run quickish ./dist. If a dist/ already exists, plain quickish finds and publishes it.

By asking an AI

Connect Quickish to ChatGPT or Claude once, then just say “publish this to Quickish” in any chat and your page goes live, assets and all. See Publish with AI for the full setup.

“Build a one-page site as a single self-contained index.html, then publish it to Quickish.”

What happens next

Your page gets a URL you can share immediately. From there you can change who can see it, point a custom domain at it, or add a backend to make it interactive.

Getting started

Pages & folders

A page is a folder. Quickish gives it a live URL and serves every file inside.

On a personal account your home page is <you>.quickish.site/, with optional sub-pages at /name/. Publish a sub-page by naming it: quickish ./site deck serves at /deck/. (Work accounts are covered in Workspace accounts & URLs.)

Folders & index.html

Drop a whole folder and the links between files inside it just work, relative paths and all. Keep an index.html at the root so the page has a homepage. If there isn't one, Quickish picks your most recently edited HTML file as the homepage and tells you which.

Single documents render automatically

Publish a single document with no page of its own (a lone PDF, Markdown, text, CSV, or image), and Quickish builds a clean in-browser viewer for it instead of handing over a bare file. A Download button for the original is always one click away.

FileYou get
.pdfA paged PDF reader (rendered to canvas, so nothing in the file can run).
.md .markdownThe Markdown rendered as a styled article (sanitized, no embedded scripts).
.csv .tsvA sortable table.
.txt .logA clean monospace reading view.
imagesA centered, full-bleed viewer.
$ quickish ./resume.pdf     # a single PDF → a real PDF reader at your URL
$ quickish ./notes.md       # Markdown → a styled, readable page

It only kicks in when the document is the whole publish (no index.html). The viewer inherits the page's visibility, so a private PDF stays private. Each viewer loads only what it needs, served first-party.

How many pages render

On the free plan, one HTML file per site renders (its index.html); CSS, JS, and images always load, so single-page sites work perfectly. Paid plans render every HTML page in the folder, so multi-page sites navigate freely. See Plans.

Getting started

Sharing & visibility

Every page can be private, shared with named people, company-wide, or fully public.

Default when you don't choose: a work-email account publishes company-visible (anyone signed in at your domain); a personal account publishes public (after a one-time content-policy consent).

Override it per page in the dashboard, or with a flag on the CLI:

ModeWho can view
--privateOnly you.
--invites a@co.com,b@co.comYou and the people you list (they sign in to prove it).
--workspaceAnyone signed in at your company domain. default · work
--publicAnyone with the link, after a one-time content-policy consent. default · personal

Preview, then promote

When an AI assistant publishes for you (via the Quickish connector), the new app lands as a private preview: only you can see it while you iterate, real backend and all. When it's ready, open the dashboard and hit Promote on the page card to make it live in one click.

Promoting only flips who can view it; the page and its data carry over untouched, because the preview is the page. Publishing from the CLI is direct: it uses the visibility you pass (or the account default), with no preview step.

Remix an app

See a Quickish app you like? Remix it into your own copy. In the dashboard, paste the app's URL and hit Remix, and you get a brand-new private preview under your account (with its own fresh backend) to edit and then promote.

You can remix any public app, or one shared inside your own company. A remix starts with an empty database and never copies the original's live data, unless the source app marks a collection as sample data, in which case those example records come along so your copy isn't blank on first run.

Guides

Publish with AI

AI assistants can generate a whole page, but the result usually has nowhere to live. Connect Quickish once, then just say “publish this to Quickish” and your page goes live, assets and all.

Claude · custom connector

Add Quickish as a connector and sign in once. Then publish from any chat.

  1. In Claude, open Settings → Connectors and choose Add custom connector.
  2. Set the URL to https://quickish.website/mcp and connect.
  3. Sign in with Quickish when prompted, then say “publish this to Quickish” in any conversation.
Build a real app, not just a page. The connector also gives Claude tools to read, seed, and write your page's quick.db collections, so you can prototype a stateful app end to end: “build a sprint-retro board with live columns, seed three example cards, and publish it private.” Claude writes the HTML (using quick.db + quick.realtime + quick.identity()), seeds the data, and hands back a live URL.

ChatGPT · custom GPT

Requires ChatGPT Plus. Create a GPT with one action and OAuth; you (and anyone you share it with) sign in once with Quickish, with no tokens to pass around.

  1. ChatGPT → Explore GPTsCreateConfigure. Name it Quickish Publisher.
  2. Paste instructions telling it to produce one self-contained index.html and call publishToQuickish.
  3. Under Actions, add an OpenAPI schema pointing at https://api.quickish.website with a POST /_assistant/publish operation.
  4. Set Authentication to OAuth, with the Authorization URL https://quickish.website/oauth/authorize, Token URL https://quickish.website/oauth/token, scope publish.
  5. Save and Publish the GPT. Everyone you share it with signs in with their own Quickish account.

The exact instructions and schema to paste are in your dashboard under Settings → Publish with AI.

Coding agents & the CLI

Assistants with a terminal (e.g. Claude Code) can publish for you. Sign in once with quickish login, then ask:

“Build a one-page site as a single self-contained index.html, then publish it to Quickish with the quickish CLI. Also invite alex@acme.com.”

It writes the file, runs quickish (passing --invites when you name people), and hands you the live link.

Advanced · direct API

For your own scripts: grab a publish token in your dashboard under Settings → Publish with AI, then POST a self-contained page. Full details in the HTTP API reference.

$ curl -X POST https://api.quickish.website/_assistant/publish \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "html": "<!doctype html><h1>hi</h1>" }'

Returns { "url": "..." }. Optional fields: page, share (private / public / workspace), invites.

Guides

Add a backend

Any page gets a zero-config backend by adding one script tag. No keys to manage, nothing to provision.

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

That one tag gives you quick.db (a document store), quick.files (uploads), quick.identity() (the signed-in visitor), quick.realtime() (live updates), and quick.cast() (push to a screen from your phone). This page covers the document store; each API has a precise reference too.

Store and read documents

A collection is a named bucket of JSON documents. Create, list, update, remove, and subscribe for live changes:

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). Values are always parameters, so a query can never inject anything. Full options in the quick.db reference.

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:

// 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"],
});

The full vocabulary of who's who lives in Permissions.

Sample data for remixes

When someone remixes your app, their copy starts empty. 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).

Reaching your data from outside

The same collections are reachable off-page (for a script or an AI agent that builds your app) using a data token scoped to one page. See the HTTP API reference. With the Claude connector you don't even need a token; Claude reads and writes through built-in tools.

Need real relations? When a document store starts to strain (joins, aggregates, a leaderboard), reach for quick.sql. Need a secret key or trusted writes? Use a server function.
Guides

Permissions

Two questions, for both your pages and your data: who can view it, and who can change it. Quickish picks safe answers by default, and you can override them.

The defaults: a page is editable only by you, however widely it's shared to view. A collection is readable by your page's audience and writable only by you. So a public page is not a free-for-all: visitors can read, but nothing changes unless you allow it.

The vocabulary

Both pages and collections describe access using the same principals, kinds of people relative to the page:

PrincipalWho it means
ownerYou, whoever published the page (plus anyone you name as an admin).
audienceWhoever can already view the page (public → everyone, company → your domain, invited → the people you listed).
authorThe person who created a particular record. Used for “edit only your own.”
usersAnyone signed in (with any Google account).
workspacePeople at your company (same email domain).
publicAnyone at all, signed in or not.

Who can edit a page

Editing a page means re-publishing over it. By default that's owner only: even a company-visible or fully public page can only be changed by you. Open it up for collaborators:

Edit policyWho can re-publish
--edit-policy ownerOnly you. default
--editors a@co.com,b@co.comYou and the people you list.
--edit-policy workspaceAnyone at your company domain.

This is independent of who can view the page. Re-publishing never changes who owns a page; adding editors lets people update it, not take it over.

Who can use a collection

A collection gets a policy when you declare it, one rule per operation. Each rule is a principal or a list (allowed if you match any). write is shorthand for create + update + delete.

PolicyBehaviour
{ read:"audience", write:"owner" }Everyone reads, only you write. Blog posts, a changelog, a menu.
{ read:"audience", create:"users", update:"author", delete:["author","owner"] }Signed-in visitors add; each edits only their own; you can moderate. Comments, a wall.
{ read:"author", write:"author" }Everyone sees and edits only their own rows. Per-person to-dos, drafts.
{ read:"audience", write:"audience" }Anyone who can view may write. A classic open guestbook (opt in deliberately).

admins lets you name extra people who count as owner for moderation:

quick.db.collection("comments", {
  read: "audience", create: "users", update: "author",
  delete: ["author", "owner"],
  admins: ["editor@acme.com"],   // can delete anyone's comment
});

A read returns a _mine flag on each record (true when you authored it) so your page can show “edit”/“delete” only where they'll work, without exposing anyone's email.

A note on trust: these boundaries protect your data from visitors, not from yourself. Since you control your page's code, you can always rewrite its rules. Treat them as intent, not a vault.
Guides

Server functions

Ship a tiny server-side backend with your page: code that runs off-page, holds secret keys, and makes trusted writes the browser can't forge.

A static page plus quick.db covers a lot, but everything there runs in the browser where anyone can see it. A function adds trusted JavaScript that runs on the server, so it can call a paid API with a secret key, tally a vote nobody can stuff, or validate input before it touches your data.

browser (public)  →  quick.fn("checkout")  →  your function (secret env, scoped db)  →  quick.db

One file is one function

Drop a file in a functions/ folder next to your page and it becomes a server-side endpoint. The filename is the name:

my-app/
  index.html            # your page (public, served from the edge)
  functions/
    checkout.js         # → quick.fn("checkout")
    lib/
      stripe.js         # NOT a function, a shared module the entrypoints import

Top-level files are entrypoints; anything in a sub-folder is shared code you import. Function source is stored server-side and never shipped to the browser.

Nothing to register. The files are the source of truth. Each publish reconciles the set: a new file is provisioned, a changed file updated, a deleted file torn down. Just run quickish.

The handler

A function exports a default async handler. Return a Response, or just a plain object (sent as JSON) or a string:

// functions/hello.js
export default async function (req, { identity }) {
  const me = identity();                 // the signed-in visitor, or null
  return { hello: me ? me.email : "world" };
}

The handler receives the request and a context object with identity(), env (your secrets), db (a trusted scoped quick.db), and fetch (guarded outbound calls). The full contract is in the server functions reference.

Declare what it needs

A function says what it can touch with a statically-declared config that's read at publish time, so it travels with the file (and with a remix):

// functions/checkout.js
export const config = {
  env: ["STRIPE_SECRET"],                // secret names it may read
  db:  { orders: "write", prices: "read" }, // collections + access
  egress: ["api.stripe.com"],            // hosts it may call out to
};

export default async function (req, { env, db, identity }) {
  const me = identity();
  const order = await db.collection("orders").create({ user: me?.email, items: req.body.items });
  return Response.json({ ok: true, id: order.id });
}

Secrets are set out of band (never in your code or repo), encrypted at rest, and injected only at call time. The injected db is the owner's trusted writer: it can write owner-only collections a public visitor can't, but only the collections you named. Outbound fetch is off until you list hosts in egress.

Call it from your page

const res = await quick.fn("checkout", { items: cart });
if (res.ok) showReceipt(res.id);

// GET with no body:
const status = await quick.fn("status", null, { method: "GET" });

Each invocation runs in an isolated sandbox with comfortable caps (64 MB, 10 s, 20 outbound calls, 1 MB request / 4 MB response) plus a per-account rate ceiling. A function error never breaks your page deploy.

“add a functions/contact.js that emails me when someone submits the form, with my SendGrid key as a secret”
Guides

Relational data with quick.sql

A real relational database at your page, with tables, joins, and constraints and zero provisioning. It's managed Postgres without the setup.

quick.db (documents) is the default for “just store some JSON.” quick.sql is the upgrade for when you actually have relations: a leaderboard, a kanban, anything that outgrows a document store. You get Postgres directly, behind one tagged-template call. No connection string, no setup.

Query from your page

quick.sql is a tagged template that returns the rows. Interpolated values are always bound parameters ($1, $2, …) and never spliced into the SQL, so a hostile value can never inject:

const top = await quick.sql`
  select name, sum(points) as pts
  from scores group by name order by pts desc limit 10`;

const mine = await quick.sql`select * from scores where name = ${user}`;

The browser path is read-only: a single select / with statement, automatically row-capped and time-bounded. Reads follow your page's audience, so a private page's data stays private.

Declare your schema

Schema is declared, not improvised, so it's reproducible and travels with the app. Put ordered *.sql files in a sql/ folder next to your page; they're applied at publish, in filename order, exactly once each:

my-app/
  index.html
  sql/
    001_init.sql
    002_add_index.sql
-- sql/001_init.sql
create table if not exists scores (
  id serial primary key,
  name text not null,
  points int not null default 0,
  created_at timestamptz not null default now()
);

Publish and the migrations run automatically. A re-publish is a no-op unless you add a new file; applied migrations are tracked, so existing ones never re-run.

Writes go through a function

Today, runtime writes come from a server function, your trusted server-side code, so a public visitor can read the leaderboard but only your logic can change it. (Schema and seed data come from your migrations at deploy.) Direct browser insert/update is intentionally not allowed.

How it's isolated

Each site gets its own Postgres schema, owned by a dedicated least-privilege role. A connection for your site can only ever reach your schema, so cross-site access is impossible by construction. It's plain Postgres: use the types, constraints, and functions you already know, and round-trip your data with quickish export.

quick.db or quick.sql?

Reach forWhen
quick.dbYou just want to store and read some JSON, with live updates and per-page permissions, with no schema to think about.
quick.sqlYou have real relations: joins, aggregates, constraints, a leaderboard or kanban that needs group by and order by.

They're independent: use either, or both, on the same page.

Guides

Realtime & casting

Push live updates between everyone on a page, and turn any page into a screen you control from your phone, both with nothing to provision.

Live updates with quick.realtime

Open a named channel; everyone on the page who joins it sees what you publish, instantly. It's a thin pub/sub bus over websockets:

const room = quick.realtime("lobby");
room.on(msg => console.log("got", msg));
room.publish({ type: "cheer", from: "Tony" });

If you're storing the data anyway, quick.db's subscribe() already pushes create/update/delete events for a collection. Use quick.realtime when you want ephemeral signals (presence, cursors, a buzzer) that don't need to be saved. The channel is scoped to your page, and inherits the page's visibility: a private page's channel is gated at the edge exactly like the page.

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.

Because it's built on the realtime bus, casting inherits the page's visibility: a public page is an open display (anyone with the on-screen code can push), while a private/workspace page only lets people the page is shared with connect at all. Want a custom screen or remote? Drive it directly with quick.cast.receive() and quick.cast.connect(). See the quick.cast reference.

Private-safe by default: the screen shows a code, never a QR, unless you opt in by passing your own QR generator, so a private page's URL is never handed to a third party unless you choose one.
Guides

Custom domains

Serve a Quickish page at your own address, like docs.acme.com instead of acme.quickish.space/docs, with an automatic HTTPS certificate.

A custom domain points your own host at a page you've published. Quickish gets a TLS certificate for it from Let's Encrypt on demand and serves your page there, applying the same access rules as the Quickish URL. Everything keeps working: quick.db, quick.fn, quick.sql, realtime.

Two records, one verification

Binding a domain returns the exact DNS to add: point the host at us, and prove you own it:

RecordHostValue
CNAME (sub-domain)docs.acme.comquickish.website
TXT (ownership)_quickish-challenge.docs.acme.comquickish-domain-verify=<token>

A root/apex domain (acme.com) can't be a CNAME, so it uses an A record to the Quickish IP instead, and the bind response tells you which. Don't proxy the record through a CDN (set Cloudflare to “DNS only / grey-cloud”) so the certificate challenge reaches us.

The flow

  1. Bind the host to one of your pages. You get back a verification token and the two records above.
  2. Add the records at your DNS provider: the CNAME (or A) and the TXT.
  3. Verify. We check the TXT over public DNS; once it's there, the domain goes live and the certificate is issued on the first request. DNS can take a few minutes to propagate.
Surface today: binding is done through the CLI-authenticated API (POST /_cli/domains then POST /_cli/domains/verify, both owner/editor-gated). A polished quickish domains command and a dashboard panel are on the way.

Good to know

  • A page can carry several custom domains; one domain belongs to a single Quickish account.
  • Managing a page's domains needs the same permission as editing the page (owner, or an editor).
  • The .quickish.site, .quickish.space, and .quickish.website URLs keep working alongside your custom domain.
  • Removing a domain stops serving it immediately.
Workspace

Workspace accounts & URLs

Sign in with a Google Workspace email and your whole company shares one Quickish space, and everyone publishes to their own page under it.

Your URL on a work account

On a workspace account you publish to your own page under the company space, keyed by your email handle:

jane@acme.com  →  acme.quickish.space/jane/

A plain quickish with no page name lands there, so you never overwrite a teammate. Add sub-pages under your handle by naming them: quickish ./deck talkacme.quickish.space/jane/talk/.

The bare company root <company>.quickish.space/ is the company homepage. It is reserved for the workspace admin (whoever verified the domain) on the Workspace Unlimited plan; until it is claimed, members see a placeholder there.

Visibility on a work account

The default when you don't choose is company-visible: anyone signed in at your domain can see it. You can still make any page private, invite-only, or fully public per page. The same edit policies apply, so a teammate can be added as an editor without being able to take a page over.

What's shared and what isn't

Everyone in the workspace shares the space and the live org directory, but each person's pages and their data are their own. Reaching another page's collection is a deliberate, paid-plan action; same-page access is always free.

Workspace

Org directory

Every workspace gets a live, read-only directory of its sites built in, with no setup and always current.

quick.org.sites() (the same data as quick.db.collection("org_sites").list()) returns the org's visible sites with hit counts, kept current as teammates publish. That means a company homepage or directory page never needs updating by hand:

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("");

Members see internal + public sites; the public sees only public ones. Full field list in the quick.org reference.

“build a dynamic directory of our company's Quickish sites, ranked by views”
Workspace

Plans & seats

Free hosts a single live page. Paid keeps every page you publish live at once.

PlanPriceStorage / BandwidthIncludes
Free$01 GB / 10 GB per moOne live page, public or private, all three publish methods.
Personal Unlimited$7.99/mo5 GB / 50 GB per moUnlimited live pages & sub-pages.
Workspace Unlimited$99/mo100 GB / 1 TB per moUnlimited live pages for the whole company. 100 publishers (unlimited viewers); add more for $3/mo or $24/yr each.

On free, across sites your latest published page stays live; the rest stay saved and come back the moment you upgrade. Paid serves them all at once.

Publishers & seats

A publisher is anyone in your org who owns a live site. The workspace plan includes 100 publishers and unlimited viewers, so most teams never hit it. Anyone signed in at your domain can publish up to that limit.

Past 100, the workspace admin (whoever verified the domain) decides in the dashboard: either free up a seat, or turn on per-publisher billing at $3/mo ($24/yr) for each publisher beyond 100, optionally capping the total. Until then, publishing is blocked at 100.

API reference

The quick global

Add one script tag and a quick object appears on the page. Everything below hangs off it, with no imports, keys, or configuration.

<script src="/quick.js"></script>
APIWhat it does
quick.dbDocument store: collections of JSON with filter/sort, permissions, and live subscriptions.
quick.sqlRead-only relational SQL (Postgres) as a tagged template.
quick.realtimeA pub/sub channel over websockets for ephemeral live signals.
quick.castTurn the page into a screen you push to from your phone.
quick.filesUpload a file and get back a URL.
quick.identityThe signed-in visitor, or null.
quick.fnCall a server-side function by name.
quick.orgWorkspace-only: the org's live site directory.

Almost everything returns a Promise, so use await. All calls are scoped to the page they run on and run through the same access rules as the page itself. Trusted, off-page logic lives in server functions; reaching a page's data over plain HTTP is the HTTP API.

API reference

quick.db

A document store. You work with named collections of JSON documents; each document gets an id and a created_at automatically.

quick.db.collection(name, options?)

quick.db.collection(name: string, options?: object) → Collection

Returns a handle to the collection. options is only needed to declare a permission policy or to mark the collection as remix seed data, and only the owner page's declaration counts.

OptionTypeMeaning
readprincipal | principal[]Who can read. Default "audience".
create / update / deleteprincipal | principal[]Who can do each write op. Default "owner".
writeprincipal | principal[]Shorthand for create + update + delete.
adminsstring[]Extra emails that count as owner.
seedbooleantrue ships this collection's docs into a remix (max 500).

Principals: owner · audience · author · users · workspace · public. See Permissions.

.list(query?)

collection.list(query?: object) → Promise<Document[]>

Returns an array of { id, ...fields }. With no query it returns the collection (capped at 500 rows). The query filters server-side:

FieldExampleMeaning
where{ done: false }Top-level field equality. Operators: { votes: { gte: 3 } } with gt, gte, lt, lte, ne, in.
order"-created_at"Sort field; prefix - for descending. Accepts an array for multi-sort.
limit20Max rows (server cap 500).
offset40Skip rows, for paging.
await tasks.list({ where: { done: false }, order: "-created_at", limit: 20 });

.create(doc) · .update(id, patch) · .remove(id)

collection.create(doc: object) → Promise<Document>
collection.update(id: string, patch: object) → Promise<Document>
collection.remove(id: string) → Promise

create returns the stored document (with its new id and created_at). update merges the patch into the existing document rather than replacing it. All three are subject to the collection's write policy.

const t = await tasks.create({ title: "Ship it", done: false });
await tasks.update(t.id, { done: true });   // merges, title is untouched
await tasks.remove(t.id);

.subscribe(handlers)

collection.subscribe(handlers: { onCreate?, onUpdate?, onDelete? }) → unsubscribe()

Opens a websocket and calls your handlers as documents change. onCreate and onUpdate receive the document; onDelete receives the id. The return value is a function that closes the subscription.

const off = tasks.subscribe({
  onCreate: t => addRow(t),
  onUpdate: t => patchRow(t),
  onDelete: id => dropRow(id),
});
// later: off();

Reads serve a pre-baked snapshot on first paint (so the page isn't blank), then go live, so your first list() is instant and subsequent ones fetch fresh.

API reference

quick.sql

A tagged template for running read-only SQL against your page's own Postgres schema. Resolves to the array of result rows.

quick.sql`select ... ${value}` → Promise<Row[]>

Write SQL inside the template literal. Any ${value} you interpolate becomes a bound parameter ($1, $2, …) and never spliced into the SQL text, so a hostile value can't inject. The call resolves to an array of row objects, and rejects (throws) if the query errors.

const rows = await quick.sql`
  select name, sum(points) as pts
  from scores group by name
  order by pts desc limit 10`;

const mine = await quick.sql`select * from scores where name = ${user}`;
PropertyDetail
StatementsA single select or with from the browser. Read-only.
LimitsAutomatically row-capped and time-bounded.
VisibilityFollows the page's audience, so a private page's data stays private.
Writes / DDLSchema and seeds via sql/NNN_*.sql migrations; runtime writes via a function.

Each site has its own isolated Postgres schema owned by a least-privilege role, so cross-site access is impossible by construction. See the quick.sql guide for schema migrations and the db-vs-sql decision.

API reference

quick.realtime

A lightweight pub/sub channel over websockets, scoped to your page. Use it for ephemeral signals you don't need to store.

quick.realtime(channel: string) → { on(cb), publish(data) }

Join a named channel and you get two methods: on(cb) registers a callback that fires with each published message (already parsed from JSON), and publish(data) broadcasts data to everyone else on the channel.

const room = quick.realtime("lobby");

room.on(msg => {
  if (msg.type === "cheer") confetti(msg.from);
});

room.publish({ type: "cheer", from: "Tony" });
MethodDetail
on(cb)Calls cb(message) for each message published to the channel.
publish(data)Sends data (any JSON-serializable value) to the channel.

The channel is scoped to (your page, this channel name) and inherits the page's visibility, so a private page's channel is gated at the edge exactly like the page. For persisted changes, use quick.db's subscribe() instead; for a screen-and-remote experience, use quick.cast (built on this bus).

API reference

quick.cast

Turn the page into a cast target (a screen pushed to from a phone), or, when the URL carries a pairing code, into the remote. Built entirely on the realtime bus.

quick.cast(options?)

quick.cast(options?: object) → Screen | Remote

The all-in-one entry point. It reads the URL: with no pairing code it sets the page up as the screen (returns a Screen); when the URL has #qkcast=CODE it becomes the remote (returns a Remote). The same page is both.

<script>quick.cast()</script>

quick.cast.receive(options?)

quick.cast.receive(options?) → { code, channel, render, stop }

Force the page into screen mode. Shows a standby card with a pairing code and link until a remote pushes content.

OptionMeaning
titleHeading shown on the standby card. Defaults to the page title.
codeUse a fixed pairing code instead of a random one.
qr(link)Return an image URL to show a scannable QR. Opt-in, so a private URL is never sent to a third party unless you choose one.
render(msg, mount)Render pushes yourself instead of the built-in viewers. mount is the full-screen stage element.
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>`;
  },
});

quick.cast.connect(code, options?)

quick.cast.connect(code: string, options?) → { send, stop }

Force the page into remote mode for a given code (defaults to the code in the URL). Renders a phone-friendly panel, and gives you send(msg) to push your own messages and stop() to disconnect.

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 (no I/L/O/0/1) and live only for the session. Casting inherits the page's visibility.

API reference

quick.files

Upload a file from the browser and get back a URL you can store or display.

quick.files.upload(file)

quick.files.upload(file: File | Blob) → Promise<{ url, ... }>

Pass a File (e.g. from an <input type="file">) or a Blob. It's sent as multipart form data and resolves to a record describing the stored file, including its url. Uploads count against your plan's storage.

<input type="file" id="pick">
<script>
  pick.onchange = async () => {
    const { url } = await quick.files.upload(pick.files[0]);
    await quick.db.collection("photos").create({ url });
  };
</script>

Stored files are served first-party and follow your page's visibility.

API reference

quick.identity

Find out who is viewing the page: the signed-in visitor, or null if they're anonymous.

quick.identity()

quick.identity() → Promise<{ email, tenant, site } | null>

Resolves to the current visitor's identity, or null when no one is signed in. The same identity a server function sees via its identity() helper.

const me = await quick.identity();
if (me) greet(me.email);
else showSignInButton();
FieldMeaning
emailThe signed-in person's email.
tenantTheir workspace/tenant (for company accounts).
siteThe site the identity is scoped to.

Identity is a fact about who's viewing; it doesn't grant access by itself. What a visitor can read or write is decided by your collection policies.

API reference

quick.fn

Call one of your page's server functions by name and get its reply back.

quick.fn(name, body?, options?)

quick.fn(name: string, body?: any, options?: { method }) → Promise<any>

Invokes functions/<name>.js. By default it POSTs body as JSON; the result is parsed as JSON when the function replies with JSON, otherwise returned as text.

ArgumentDetail
nameThe function's filename without .js (route-safe: a–z, 0–9, -).
bodySent as a JSON request body. Omit or pass null for none.
options.methodOverride the HTTP method, e.g. "GET" (no body sent).
const res = await quick.fn("checkout", { items: cart });
if (res.ok) showReceipt(res.id);

// GET with no body
const status = await quick.fn("status", null, { method: "GET" });
API reference

quick.org workspaces

Workspace accounts get a live, read-only directory of the org's sites with hit counts, with no setup and always current.

quick.org.sites()

quick.org.sites() → Promise<Site[]>

Returns the org's visible sites. Identical to quick.db.collection("org_sites").list(). Members see internal + public sites; the public sees only public ones.

FieldMeaning
idSite identifier.
nameDisplay name.
urlLive URL.
ownerWho publishes it.
visitsLive hit count.
visibilitypublic / workspace / private.
isHomeWhether it's the company homepage.
updatedLast publish time.
const sites = await quick.org.sites();
sites.sort((a, b) => b.visits - a.visits);

See the org directory guide for a full directory page example.

API reference

Server functions

The contract for a functions/<name>.js file: what your handler receives, what it returns, and how it declares what it's allowed to touch.

The handler

export default async function (req, ctx) → Response | object | string

Export a default async function. Return a Response, or just a plain object (sent as JSON) or a string.

req

FieldMeaning
methodHTTP method.
pathRequest path.
queryParsed query-string object.
headersRequest headers.
bodyParsed JSON the page sent, or null for GET.

ctx

FieldMeaning
identity()The signed-in visitor as { email, tenant, site }, or null. Same identity the page sees.
envYour declared secret values, the real secrets the browser never sees.
dbA trusted, pre-scoped quick.db (same methods) that can write the owner-only collections you declared.
fetchA guarded outbound fetch, off unless you allow hosts in config.egress.

Return helpers mirror the web platform: Response.json(obj), Response.redirect(url), or new Response(body, { status, headers }).

export const config

export const config = { env, db, egress }

A statically-declared object, read at publish time, that travels with the file. It's how a function says what it's allowed to touch.

KeyTypeMeaning
envstring[]Secret names it may read (A–Z, 0–9, _). Values are set out of band.
db{ collection: "read" | "write" }Collections it may touch and at what level ("write" implies read).
egressstring[]Hosts ctx.fetch may call (["api.stripe.com", "*.openai.com"], or ["*"]).
export const config = {
  env: ["STRIPE_SECRET"],
  db:  { orders: "write", prices: "read" },
  egress: ["api.stripe.com"],
};

export default async function (req, { env, db, identity }) {
  const me = identity();
  const order = await db.collection("orders").create({ user: me?.email, items: req.body.items });
  return Response.json({ ok: true, id: order.id });
}

Limits

Per invocation: 64 MB memory, 10 s wall-clock, up to 20 outbound calls, 1 MB request / 4 MB response. Per account: 120/min, 2k/hour, 20k/day. A function error surfaces as a warning and never breaks your page deploy. Call functions from the page with quick.fn; the full walk-through is the Server functions guide.

API reference

HTTP API & tokens

Reach a page's data and publishing from outside the browser, whether a script, a CI job, or your own agent, with a scoped token.

Data tokens: read & write a page's collections

In your dashboard under Settings → Data tokens, mint a token scoped to one page (read/write or read-only). It goes through the exact same access rules as the in-page SDK and can only ever narrow access, never widen it. Revoke it anytime.

# 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

The same query string as the SDK works here: ?where=<json>&order=-created_at&limit=20. With the Claude connector you don't even need a token; Claude reads and writes through built-in tools.

Publish tokens: put a page online

Grab a publish token in your dashboard under Settings → Publish with AI, then POST a self-contained page:

curl -X POST https://api.quickish.website/_assistant/publish \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "html": "<!doctype html><h1>hi</h1>", "share": "public" }'

Returns { "url": "..." }. Optional fields: page (named sub-page), share (private / public / workspace), invites (emails).

CLI-authenticated endpoints

Some operations are gated to a signed-in CLI session (owner/editor only) while their polished commands land: setting function secrets (POST /_cli/fn-secrets) and binding custom domains (POST /_cli/domains, /_cli/domains/verify).