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

Start here is the plain-language path with no code: put a page online, and build a working app by asking an assistant. Getting started covers the mechanics, including the command line. Guides go a step deeper on each capability. Workspace covers shared company accounts. The API reference documents every quick.* call precisely, for when you want the exact signature.

New here and not a coder? Read Put a page online next. Prefer the terminal? Jump to Publish your first page.
Start here

Put a page online

You do not need to be a developer to have a real web page. If you have a file, or an idea an assistant can turn into one, you can have a link to share in a couple of minutes.

Two ways in

Both give you a live page at your own Quickish address. Pick whichever fits how you work:

Drop a file

Sign in and drag a file or folder onto your dashboard. It goes live right away.

Ask an assistant popular

Tell an AI what you want. It writes the page and publishes it for you.

Drop a file or a folder

Sign in with Google, then drag what you have onto the drop area on your dashboard. There is nothing to configure. A few things you can drop:

  • A folder you exported from a website builder or design tool.
  • A single page a friend or a tool made for you (an index.html).
  • A lone document like a PDF, a spreadsheet, or an image. Quickish shows it in a clean reader with a download button, covered in Pages & folders.

If you drop a folder, keep a file named index.html at the top so Quickish knows which page is the front door. If there is not one, it picks your most recent page and tells you which.

Or just ask an assistant

Connect Quickish to Claude or ChatGPT one time (the short setup is in Publish with AI). After that, say what you want in plain words and the page appears:

“Make a one-page site for my dog-walking business with my rates and a contact email, then publish it to Quickish.”

The assistant writes the page and puts it online as a private preview, a version only you can see. Look it over, ask for any changes, and when you are happy click Promote on the dashboard to make it public.

Share the link

Every page can be kept to yourself, shared with a few named people, shown to your whole company, or opened to anyone with the link. You choose per page. The options are laid out in Sharing & visibility.

Change it later

To update a page, edit your file and drop it again, or ask the assistant for another change and republish. Your files are always yours: you can pull them back down any time from your dashboard or the command line.

Want the page to actually do something? A guest book, a poll, an RSVP, a shared board. That is the next page, Build an app without code.
Start here

Build an app without code

A page can do more than sit there. With Quickish an assistant can build you a working app, save what people enter, and update live for everyone, with the database already wired up. You describe it; the assistant builds it.

What counts as an app here

Anything that remembers what people do and shows it back. A few that take minutes to make:

  • An RSVP or a sign-up sheet with a live count.
  • A guest book or a message wall.
  • A poll that tallies votes as they come in.
  • A shared board for a team, like a sprint retro or a simple kanban.
  • A leaderboard for a game or a challenge.

All of them save data and update in real time for everyone on the page, with nothing for you to set up or manage.

How it works

Connect Quickish to your assistant one time (see Publish with AI). Then:

  1. Describe the app in plain words.
  2. The assistant writes it using Quickish's built-in storage and live updates, and adds a few example entries so it is not blank on first run.
  3. It publishes a private preview, a version only you can see.
  4. You look it over, ask for changes, then click Promote to share it.
“Build a small RSVP page where guests type their name and whether they are coming, show a live count of yes and no, and publish it privately so I can try it first.”

Start from an app that already exists

You do not have to start from a blank page. Many Quickish apps are marked remixable: open one you like, click Remix, and you get your own private copy to change, with a fresh empty database of its own. It is the fastest way to get something working that you can then make your own. The official example apps are open source, so the whole thing is yours to read and adjust. More in Sharing & visibility.

When you want to go further

Everything the assistant builds is a normal Quickish page, and the code is yours. When you are ready for more control, a developer or a coding assistant can take it further: real relationships with quick.sql, secret keys and trusted logic with server functions, or a full walk-through of the tools in Add a backend.

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. On a remixable page, click the Remix button in the corner; or open your dashboard, paste the app's URL into Remix an app, and hit Remix. Either way you get a brand-new private preview under your account, with its own fresh backend, to edit and then promote.

There are plenty of Quickish sites and apps you can remix; remixable is on by default for org-wide sites and apps. A remix starts with an empty database and never copies the original's live data, unless the source marks a collection as sample data, in which case those example records come along so your copy isn't blank on first run.

A remix stays linked to the app it came from, so it's a living fork. When the creator ships changes, your dashboard shows that an update is available on the page card, and one click pulls the new version in. From the terminal it's quickish remix status to see what changed and quickish remix update <folder> to merge it into your copy (clean changes only; anything you edited yourself is left for you to reconcile).

The official apps are open source. Every app Quickish ships as a remixable example is MIT licensed. Remix it or download the code (below) and the full source is yours to read, change, and ship. You can make your own apps remixable too, under whatever terms you choose.

Download your code

Your published pages are always yours to pull back down. quickish download <page> <folder> writes a page's files into a local folder, so you can edit them in your own tools and re-publish:

$ quickish download blog ./blog     # pull the "blog" page into ./blog
$ quickish ./blog blog               # publish your edits back

Leave the page name off to pull your home page. Together with remix, this is the whole point: fork an official app, download it, and the entire source is on your machine.

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

Encrypted data

Store secrets on a page that even Quickish can’t read. The data is encrypted in the visitor’s browser under a key we never see, so it’s safe at rest and yours alone.

An encrypted collection is a normal quick.db collection that’s sealed. You unlock a vault (a key) with a passphrase, then read and write through it; encrypting and decrypting happen automatically in the page. Quickish only ever stores ciphertext, so a database dump or a breach reveals nothing. Exact signatures live in the quick.crypto reference.

Zero-knowledge, by construction. The key is derived from a passphrase (or a passkey) in the browser and never reaches our servers. “We can’t read it” isn’t a policy, it’s math.

What it’s for

  • A team secret store: org-wide API keys, SSO secrets, that one shared password, kept in a page instead of a spreadsheet.
  • Private notes & vaults: journals, client records, anything that should never be readable on the server.
  • Sensitive form data that stays sealed end to end and is shared only with the people you choose.

The easy way

Create a vault locked by a passphrase, then use it like any collection:

// 1. create a vault, locked by a passphrase (at least 12 characters)
const vault = await quick.crypto.create("secrets", passphrase);
// 2. write, encrypted in the browser before it leaves the page
await vault.db("keys").create({ name: "stripe", value: "sk_live_…" });
// 3. read, fetched as ciphertext, decrypted locally
const keys = await vault.db("keys").list();

Later, on any device, unlock the same vault with the passphrase:

const vault = await quick.crypto.unlock("secrets", passphrase);
Pick a strong passphrase, and don’t lose it. Because we can’t read your data, we can’t reset it either. quick.crypto.suggest() generates a strong one; store it in a password manager, and set up recovery (below) if a team depends on it. We only require a sensible minimum length, with no “1 capital, 1 symbol” rules (those just push people to sticky notes).

Share it with your team

Sharing hands someone the key, sealed to them, so it never passes through us in the clear. Each person enrolls a sharing identity once:

// each teammate, once
await quick.crypto.enroll(passphrase);
// the owner grants a teammate (offline-friendly; they pick it up next sign-in)
await vault.share("teammate@acme.com");
// the teammate opens the shared vault
const id = await quick.crypto.identity(passphrase);
const vault = await quick.crypto.unlockShared("secrets", id);

Need access right now? Someone can request it live and an online key-holder approves, comparing a short fingerprint out loud so a tampered request can’t slip through. See requestAccess and serveGrants in the reference.

Revoke & recover

Removing a grant stops handing out the key, but anyone who already had it kept a copy. Rotate to truly cut them off: it mints a new key, re-encrypts everything, and re-shares only with whoever remains.

await vault.rotate({ remove: ["former@acme.com"] });

So a lost passphrase doesn’t mean lost data, an owner can set an optional escrow passphrase: a break-glass key held out-of-band (a safe, a password manager, split among admins). It’s off by default, because it’s the one thing that can soften “we can’t read it”; its strength is exactly the strength of that passphrase.

await vault.setEscrow(escrowPassphrase);                  // once, to enable recovery
// later, if the normal passphrase is gone:
const vault = await quick.crypto.recoverWithEscrow("secrets", escrowPassphrase);
await vault.rekey(newPassphrase);                          // set a fresh passphrase

A passkey (Face ID, Touch ID, a security key) can also unlock a vault with no passphrase to remember, where the browser supports it. experimental

What’s different about a sealed collection

Because the server only sees ciphertext, a few things deliberately don’t work, and they fail loudly rather than leak:

  • No server-side where / order. Fetch rows and filter or sort after they decrypt in the page.
  • No pre-baking (the SEO snapshot); there’s nothing readable to bake.
  • No server functions reading it; functions run on our servers, so they’re kept out of the plaintext. If a function needs a secret, copy that one value into its function secrets yourself.
  • Writes must be sealed; plaintext sent to an encrypted collection is rejected.
“Build a private team vault page where I can save org API keys, share them with teammates by email, and rotate the key when someone leaves.”
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" });

Run it on a schedule

A function can also run on its own, on a timer. Add a trigger in your app's settings file, quickish.json, keyed by the function it fires:

// quickish.json
{
  "name": "Flipboard",
  "timezone": "America/Los_Angeles",        // optional default for time-of-day
  "triggers": {
    "weather": { "type": "timed", "value": "15m" },                       // every 15 minutes
    "digest":  { "type": "timed", "value": "9:00am", "daysOfWeek": ["mon", "wed", "fri"] }
  }
}

The schedule lives in the settings file, not the function's config, so it travels with the app (and with a remix). A trigger's value is either a frequency (30s, 5m, 1h, "10 minutes", with a floor of 30 seconds) or a time of day (9:00am, 13:00, 1pm) that runs once a day. Add daysOfWeek to limit which days, or omit it to run every day. A time-of-day trigger resolves in the trigger's own tz, then the app timezone, then your account's timezone. The type is "timed" today; route triggers come later.

A scheduled run executes the same function as the page owner, so identity() is you and owner-only db writes work. It arrives as a POST with a header that marks it a timer run, so you can tell it from a page call:

// functions/weather.js
export default async function (req, { db, fetch }) {
  const scheduled = req.headers["x-quickish-trigger"] === "timed";
  const r = await fetch("https://api.open-meteo.com/v1/forecast?…");
  await db.collection("weather").create({ at: Date.now(), reading: await r.json() });
}

A run that throws, times out, or returns a 5xx counts as a failure; five in a row pauses the schedule until your next publish re-arms it. Running quickish dev fires the same triggers on plain timers, so you can test a schedule offline before you publish.

The flipboard demo uses a scheduled function to refresh its weather every 15 minutes, instead of fetching it on every page load.

Run it from a queue

Set config.queue when a function should process background messages. Quickish starts it when work is ready and handles success, retries, and worker limits. See the quick.queue guide.

Limits

Each invocation runs in an isolated sandbox with comfortable caps, so ordinary API calls and trusted writes fit comfortably while runaway code gets contained.

LimitDefault
Memory64 MB heap per invocation
Time10 s wall-clock ceiling, 5 s CPU deadline
Outbound fetchesUp to 25 subrequests per invocation
Request body1 MB into the function
Response body4 MB out of the function

There is also a per-account rate ceiling: 120 calls per minute, 2,000 per hour, and 20,000 per day. A function error surfaces as a warning and 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

Background work with quick.queue

Put work in a queue now and process it safely when a worker is free.

A queue is useful when a task should not make someone wait. Your page adds a message, then a server function processes it in the background. Common examples include sending email, resizing images, importing files, and calling a slow API.

Send a message

Send any value that JSON can store:

await quick.queue("emails").send({
  to: "sam@example.com",
  subject: "Welcome!"
});

Process messages with a function

Create a server function and name the queue in its config:

// functions/send-email.js
export const config = { queue: "emails" };

export default async function (message) {
  await sendEmail(message);
}

Publish with quickish. There is no queue to create in a dashboard. Quickish starts the queue when the first message arrives and runs the function when work is ready.

Success is automatic. If the function finishes without an error, Quickish removes the message. If it throws or returns an error response, Quickish tries again.

The useful defaults

  • A message can stay in the queue for 24 hours.
  • A failed message is retried up to 3 times.
  • One copy of the function runs at a time.
  • Retries wait longer after each failure.

Queue delivery happens at least once. A function may receive the same message again after a timeout or restart. Make repeated work safe when possible. For example, store an order ID and check whether it was already completed.

Change lifetime and retries

Pass options when one message needs different rules. Times use milliseconds.

await quick.queue("emails").send(message, {
  ttlMs: 2 * 60 * 60 * 1000, // keep for 2 hours
  maxRetries: 8
});

ttlMs can be from 1 second to 30 days. maxRetries can be from 0 to 25. A message is removed when its lifetime ends, even if it is waiting for another retry.

Process more at once

Start with one worker. Add more when each message is independent and the service you call can handle the extra traffic.

export const config = {
  queue: {
    name: "emails",
    maxAsyncWorkers: 10,
    batchSize: 5
  }
};

export default async function (message) {
  await sendEmail(message);
}

maxAsyncWorkers is the number of function copies that may run at once. The default is 1 and the maximum is 100. batchSize is the number of messages each worker processes before it stops. The handler still receives one message at a time.

Take one in browser code

Most apps should use a server function. A signed-in browser can also take one message when a person or browser tool needs to do the work:

const job = await quick.queue("imports").take();

if (job) {
  try {
    await importFile(job.message);
    await job.ack();
  } catch (error) {
    await job.retry(error);
  }
}

take() returns null when the queue is empty. Taking a message hides it for 30 seconds. ack() removes it. retry() puts it back after a short wait. If the browser closes, the message becomes available when the lease ends.

const job = await quick.queue("imports").take({
  visibilityTimeoutMs: 2 * 60 * 1000
});

Names, limits, and local work

Queue names use lowercase letters, numbers, dots, underscores, and hyphens. They start with a letter and can be up to 63 characters. A message can be up to 256 KB.

Queues belong to one Quickish page. A page cannot read or process another tenant's queue. Browser queue calls require a signed-in visitor. Server functions keep the same secret, database, network, memory, and time limits described in Server functions.

quickish dev includes an in-memory queue and runs queue functions automatically. Use the same code locally, then publish without changing the queue setup. The local queue starts empty each time you restart quickish dev.

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.
Guides

Develop locally

Two commands let you build on your own machine and still use everything Quickish gives a page. quickish dev runs the whole backend locally. quickish --localish puts the site running on your laptop on a real, shareable URL.

Run Quickish on your machine

Run quickish dev in your page's folder and it serves the page locally with the full quick.* surface running for real, just on your machine instead of in the cloud:

$ quickish dev
  → your page on http://localhost:4321 with a live backend

Everything works the same as it will once you publish, with no keys and no round-trip to a server:

If your page is a framework app (Vite, Next, Astro), quickish dev notices, starts it for you, and keeps hot reload working, so you edit source and see changes instantly with the backend attached.

Work against known data. Save a snapshot of your local database and restore it later, so you can develop and demo against the same fixtures every time instead of a fresh empty store.

Share your localhost New

Sometimes you want someone else, a teammate, a client, or your own coding assistant, to see the site running on your laptop right now, before it is published anywhere. quickish --localish opens a secure tunnel and gives your local site a public Quickish URL:

$ quickish --localish
  → https://you--myapp.qksh.dev  (points at your laptop, live)

It finds the dev server you already have running and puts it on the web. The link stays put while the command runs, so you can hand it out and keep pushing changes behind it.

A shorter, nicer name

Every tunnel gets a stable address like you--myapp.qksh.dev for free. On a paid plan you can claim a short vanity name and reuse it:

$ quickish --localish --claim myapp
  → https://myapp.qksh.dev

Claim and release names from your dashboard too. A name belongs to your account and can be reused across sessions; drop it when you are done and it goes back in the pool.

Who can reach it

A tunnel follows sign-in the same way your pages do, so a private project stays behind a sign-in gate rather than open to the whole internet. For a tool or a teammate who should not have to sign in, hand out the agent link from the dashboard's Share dialog, which grants access without a login. Run it detached with --background, check on it with --status, and take it down with --stop.

A tunnel is not a publish. The URL is live only while your machine is. When the site is ready for good, publish it so it is hosted on Quickish and stays up on its own.
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.queueDurable background messages with retries, lifetimes, and automatic function workers.
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.queue

Send durable messages, process them with server functions, or lease one in signed-in browser code.

quick.queue(name)

quick.queue(name: string) → Queue

Returns a handle for a queue owned by the current page.

.send(message, options?)

queue.send(message: JSON, options?: { ttlMs?, maxRetries? }) → Promise<MessageInfo>

Adds one message. The default lifetime is 24 hours and the default retry limit is 3. Messages can be up to 256 KB.

.take(options?)

queue.take(options?: { visibilityTimeoutMs? }) → Promise<Job | null>

Leases the next ready message for a signed-in browser. Returns null when none is ready. The default lease is 30 seconds.

The Job object

Field or methodMeaning
messageThe JSON value sent by the producer.
id / attemptsThe message ID and current delivery attempt.
expiresAt / leaseUntilWhen the message expires and when this lease ends.
ack()Marks the work complete and removes the message.
retry(error?)Records a failure and makes the message available again after a wait.

For automatic processing, export config.queue from a server function. See the quick.queue guide.

API reference

quick.crypto

Zero-knowledge encrypted data. Keys are derived and held in the browser; Quickish stores only ciphertext and wrapped keys. See the Encrypted data guide for the tour.

quick.crypto.create(name, passphrase)

quick.crypto.create(name: string, passphrase: string) → Promise<Vault>

Creates a new vault (a key) sealed by passphrase (minimum 12 characters) and returns a live Vault. The passphrase never leaves the browser.

quick.crypto.unlock(name, passphrase)

quick.crypto.unlock(name: string, passphrase: string) → Promise<Vault>

Opens a vault you own on this device. Rejects if the passphrase is wrong.

.suggest() · .vaults()

quick.crypto.suggest() → string
quick.crypto.vaults() → Promise<{ name, created_at }[]>

suggest returns a strong random passphrase (about 120 bits). vaults lists the vaults you own.

Sharing

quick.crypto.enroll(passphrase: string) → Promise
quick.crypto.identity(passphrase: string) → Promise<Identity>
quick.crypto.shared() → Promise<{ name, granted_by, created_at }[]>
quick.crypto.unlockShared(name: string, identity: Identity) → Promise<Vault>

enroll publishes your sharing identity once (an RSA key whose private half is wrapped under your passphrase). identity unlocks that private key so you can open vaults shared with you; shared lists them; unlockShared opens one.

quick.crypto.requestAccess(name, options?)

quick.crypto.requestAccess(name: string, options?: { timeoutMs?, onFingerprint? }) → Promise<Vault>

Asks online key-holders for the key over the realtime bus with no prior setup: a throwaway key is minted for the request, and the wrapped key is relayed back. options.onFingerprint(fp) gives you a short code to confirm with the grantor out-of-band. Resolves to an in-session vault.

Passkey unlock experimental

quick.crypto.supportsPasskeys() → boolean
quick.crypto.createWithPasskey(name: string) → Promise<Vault>
quick.crypto.unlockWithPasskey(name: string) → Promise<Vault>

Create or unlock a vault with a WebAuthn passkey instead of a passphrase, where the authenticator supports the PRF extension. Support varies, so keep the passphrase path as a fallback.

quick.crypto.recoverWithEscrow(name, escrowPassphrase)

quick.crypto.recoverWithEscrow(name: string, escrowPassphrase: string) → Promise<Vault>

Break-glass recovery using the vault’s escrow passphrase. The returned vault has no passphrase wrap yet; call vault.rekey() to set one.

The Vault object

What create, unlock, unlockShared, requestAccess, and recoverWithEscrow return:

MethodMeaning
db(name)A sealed collection: .create, .list, .update, .remove, encrypted and decrypted locally. No where / order.
share(email)Wrap the key to an enrolled member and store the grant.
grants() · revoke(email)List who has access; drop a grant (rotate to fully cut off).
rotate({ remove?, escrowPassphrase? })New key, re-encrypt every sealed doc, re-share with whoever remains.
serveGrants(approve)Approve live requestAccess calls; approve({ email, fingerprint }) returns a boolean. Returns a stop function.
setEscrow(passphrase) · removeEscrow()Enable or disable break-glass recovery.
rekey(newPassphrase)Change the vault’s passphrase (the underlying key is unchanged).

Under the hood: a per-vault AES-256-GCM key, wrapped to each member with RSA-OAEP, derived from passphrases with PBKDF2, all via WebCrypto in the browser. The server stores only ciphertext and wrapped keys.

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, queue }

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 ["*"]).
queuestring | objectA queue name, or { name, maxAsyncWorkers, batchSize }, for automatic background processing.
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 25 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).