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.
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/.
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.
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.
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.
| File | You get |
|---|---|
.pdf | A paged PDF reader (rendered to canvas, so nothing in the file can run). |
.md .markdown | The Markdown rendered as a styled article (sanitized, no embedded scripts). |
.csv .tsv | A sortable table. |
.txt .log | A clean monospace reading view. |
| images | A 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.
Sharing & visibility
Every page can be private, shared with named people, company-wide, or fully public.
Override it per page in the dashboard, or with a flag on the CLI:
| Mode | Who can view |
|---|---|
--private | Only you. |
--invites a@co.com,b@co.com | You and the people you list (they sign in to prove it). |
--workspace | Anyone signed in at your company domain. default · work |
--public | Anyone 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.
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.
- In Claude, open Settings → Connectors and choose Add custom connector.
- Set the URL to
https://quickish.website/mcpand connect. - Sign in with Quickish when prompted, then say “publish this to Quickish” in any conversation.
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.
- ChatGPT → Explore GPTs → Create → Configure. Name it Quickish Publisher.
- Paste instructions telling it to produce one self-contained
index.htmland callpublishToQuickish. - Under Actions, add an OpenAPI schema pointing at
https://api.quickish.websitewith aPOST /_assistant/publishoperation. - Set Authentication to OAuth, with the Authorization URL
https://quickish.website/oauth/authorize, Token URLhttps://quickish.website/oauth/token, scopepublish. - 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:
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.
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.
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 vocabulary
Both pages and collections describe access using the same principals, kinds of people relative to the page:
| Principal | Who it means |
|---|---|
owner | You, whoever published the page (plus anyone you name as an admin). |
audience | Whoever can already view the page (public → everyone, company → your domain, invited → the people you listed). |
author | The person who created a particular record. Used for “edit only your own.” |
users | Anyone signed in (with any Google account). |
workspace | People at your company (same email domain). |
public | Anyone 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 policy | Who can re-publish |
|---|---|
--edit-policy owner | Only you. default |
--editors a@co.com,b@co.com | You and the people you list. |
--edit-policy workspace | Anyone 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.
| Policy | Behaviour |
|---|---|
{ 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.
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.
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.
functions/contact.js that emails me when someone submits the form, with my SendGrid key as a secret”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 for | When |
|---|---|
quick.db | You just want to store and read some JSON, with live updates and per-page permissions, with no schema to think about. |
quick.sql | You 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.
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.
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:
| Record | Host | Value |
|---|---|---|
| CNAME (sub-domain) | docs.acme.com | quickish.website |
| TXT (ownership) | _quickish-challenge.docs.acme.com | quickish-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
- Bind the host to one of your pages. You get back a verification token and the two records above.
- Add the records at your DNS provider: the CNAME (or A) and the TXT.
- 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.
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.websiteURLs keep working alongside your custom domain. - Removing a domain stops serving it immediately.
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 talk → acme.quickish.space/jane/talk/.
<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.
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.
Plans & seats
Free hosts a single live page. Paid keeps every page you publish live at once.
| Plan | Price | Storage / Bandwidth | Includes |
|---|---|---|---|
| Free | $0 | 1 GB / 10 GB per mo | One live page, public or private, all three publish methods. |
| Personal Unlimited | $7.99/mo | 5 GB / 50 GB per mo | Unlimited live pages & sub-pages. |
| Workspace Unlimited | $99/mo | 100 GB / 1 TB per mo | Unlimited 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.
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>
| API | What it does |
|---|---|
| quick.db | Document store: collections of JSON with filter/sort, permissions, and live subscriptions. |
| quick.sql | Read-only relational SQL (Postgres) as a tagged template. |
| quick.realtime | A pub/sub channel over websockets for ephemeral live signals. |
| quick.cast | Turn the page into a screen you push to from your phone. |
| quick.files | Upload a file and get back a URL. |
| quick.identity | The signed-in visitor, or null. |
| quick.fn | Call a server-side function by name. |
| quick.org | Workspace-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.
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?)
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.
| Option | Type | Meaning |
|---|---|---|
read | principal | principal[] | Who can read. Default "audience". |
create / update / delete | principal | principal[] | Who can do each write op. Default "owner". |
write | principal | principal[] | Shorthand for create + update + delete. |
admins | string[] | Extra emails that count as owner. |
seed | boolean | true ships this collection's docs into a remix (max 500). |
Principals: owner · audience · author · users · workspace · public. See Permissions.
.list(query?)
Returns an array of { id, ...fields }. With no query it returns the collection (capped at 500 rows). The query filters server-side:
| Field | Example | Meaning |
|---|---|---|
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. |
limit | 20 | Max rows (server cap 500). |
offset | 40 | Skip rows, for paging. |
await tasks.list({ where: { done: false }, order: "-created_at", limit: 20 });
.create(doc) · .update(id, patch) · .remove(id)
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)
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.
quick.sql
A tagged template for running read-only SQL against your page's own Postgres schema. Resolves to the array of result rows.
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}`;
| Property | Detail |
|---|---|
| Statements | A single select or with from the browser. Read-only. |
| Limits | Automatically row-capped and time-bounded. |
| Visibility | Follows the page's audience, so a private page's data stays private. |
| Writes / DDL | Schema 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.
quick.realtime
A lightweight pub/sub channel over websockets, scoped to your page. Use it for ephemeral signals you don't need to store.
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" });
| Method | Detail |
|---|---|
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).
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?)
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?)
Force the page into screen mode. Shows a standby card with a pairing code and link until a remote pushes content.
| Option | Meaning |
|---|---|
title | Heading shown on the standby card. Defaults to the page title. |
code | Use 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?)
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.
quick.files
Upload a file from the browser and get back a URL you can store or display.
quick.files.upload(file)
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.
quick.identity
Find out who is viewing the page: the signed-in visitor, or null if they're anonymous.
quick.identity()
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();
| Field | Meaning |
|---|---|
email | The signed-in person's email. |
tenant | Their workspace/tenant (for company accounts). |
site | The 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.
quick.fn
Call one of your page's server functions by name and get its reply back.
quick.fn(name, body?, options?)
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.
| Argument | Detail |
|---|---|
name | The function's filename without .js (route-safe: a–z, 0–9, -). |
body | Sent as a JSON request body. Omit or pass null for none. |
options.method | Override 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" });
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()
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.
| Field | Meaning |
|---|---|
id | Site identifier. |
name | Display name. |
url | Live URL. |
owner | Who publishes it. |
visits | Live hit count. |
visibility | public / workspace / private. |
isHome | Whether it's the company homepage. |
updated | Last 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.
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 a default async function. Return a Response, or just a plain object (sent as JSON) or a string.
req
| Field | Meaning |
|---|---|
method | HTTP method. |
path | Request path. |
query | Parsed query-string object. |
headers | Request headers. |
body | Parsed JSON the page sent, or null for GET. |
ctx
| Field | Meaning |
|---|---|
identity() | The signed-in visitor as { email, tenant, site }, or null. Same identity the page sees. |
env | Your declared secret values, the real secrets the browser never sees. |
db | A trusted, pre-scoped quick.db (same methods) that can write the owner-only collections you declared. |
fetch | A 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
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.
| Key | Type | Meaning |
|---|---|---|
env | string[] | 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). |
egress | string[] | 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.
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).