# UniQuick — zero-config backend for AI-built sites # https://uniquick.azurewebsites.net UniQuick hosts static HTML/JS sites at https://uniquick.azurewebsites.net/s// and gives every site a backend — key-value data, file uploads, realtime WebSockets, AI chat, and silent University of Illinois SSO — with ONE script tag and NO keys. Visitors must have an @illinois.edu account; sign-in is automatic and silent. If you are an AI agent building a site for UniQuick: write a plain static site (index.html + assets, no build step needed), include the SDK script tag, and use the window.quick API below. Everything is same-origin — never configure CORS, API keys, or auth. ## The script tag (required, put it first in ) This defines window.quick. All methods implicitly wait for sign-in, so you can call them immediately at the top of your own script. ## SDK reference (window.quick) quick.ready : Promise Resolves once the visitor is signed in. Awaiting it is optional — every other method awaits it internally. quick.user : { name: string, upn: string } The signed-in visitor (after ready). Example: await quick.ready; document.querySelector("#who").textContent = quick.user.name; quick.data.get(key) -> Promise quick.data.set(key, value) -> Promise // value: any JSON quick.data.delete(key) -> Promise quick.data.list(prefix?) -> Promise // returns keys Firebase-style KV scoped to THIS site. Keys may contain slashes; use path-style keys to enable prefix listing. Example guestbook: await quick.data.set(`entries/${Date.now()}`, { text, by: quick.user.name }); const keys = await quick.data.list("entries/"); const entries = await Promise.all(keys.map(k => quick.data.get(k))); quick.files.upload(file: File) -> Promise Uploads a File and returns an absolute URL. The returned URL is a capability URL: anyone with the link can fetch the file (no sign-in needed), so it works directly in / tags. Treat it as shareable and do not upload secrets or sensitive data. Example: input.onchange = async () => { const url = await quick.files.upload(input.files[0]); img.src = url; }; quick.ws.on(event, handler) quick.ws.send(event, data) quick.ws.presence() -> Promise<{upn, name}[]> Named-event realtime within this site. One lazy socket per page, auto-reconnect. Your own send() is NOT echoed back to you; messages from others arrive with sender identity. Example shared cursors: quick.ws.on("move", (data, from) => drawCursor(from.upn, data.x, data.y)); onmousemove = (e) => quick.ws.send("move", { x: e.clientX, y: e.clientY }); quick.ai.chat(prompt, opts?) -> Promise opts: { system?: string, onToken?: (t: string) => void } Azure OpenAI chat completion. Pass onToken for streaming. Per-user daily token budget (default 100k); over-budget calls reject with code BUDGET. Example: const out = await quick.ai.chat("Summarize:\n" + text, { system: "You are concise.", onToken: (t) => (pre.textContent += t), }); Errors: rejections are QuickError instances with a .code property — AUTH (sign-in problem), PERMISSION (site is owner-write and visitor is not the owner), VALIDATION, NOT_FOUND, BUDGET (AI quota), CONFLICT. QuickAuthError and QuickPermissionError subclasses exist for instanceof checks. ## Data modes Sites default to data_mode "open": any signed-in @illinois.edu visitor can read AND write the site's data (writes are attributed). "owner-write" sites give visitors read-only data; only the owner's writes succeed (others get PERMISSION). Pick owner-write for dashboards/publishing, open for guestbooks, games, and collaborative tools. ## Creator REST API (for agents deploying sites) Auth: Authorization: Bearer . Tokens look like qk_... and come from https://uniquick.azurewebsites.net/token (browser sign-in, copy once). Errors are JSON { "error": { "code": "...", "message": "..." } } with statuses: AUTH 401, PERMISSION 403, VALIDATION 400, NOT_FOUND 404, BUDGET 429, CONFLICT 409, UNAVAILABLE 502/503, INTERNAL 500. POST /api/sites {"slug":"my-site","title":"My Site","data_mode":"open"} The server prefixes the slug with your netid ('my-site' -> 'vishal-my-site'); the response's "id" is the real site id — use it everywhere below. GET /api/sites list my sites PATCH /api/sites/:site {"title":"...","data_mode":"owner-write"} (owner only) POST /api/sites/:site/deploy {"files":[{"path":"index.html","contentBase64":"...","contentType":"text/html"}]} PUT /api/sites/:site/files/ raw body upload of one file (Content-Type respected) DELETE /api/sites/:site delete site + data (owner only) Deploy example with curl: curl -X POST https://uniquick.azurewebsites.net/api/sites \ -H "Authorization: Bearer $UNIQUICK_TOKEN" -H "Content-Type: application/json" \ -d '{"slug":"hello","title":"Hello"}' # response: {"id":"-hello", ...} — use that id below curl -X PUT https://uniquick.azurewebsites.net/api/sites/-hello/files/index.html \ -H "Authorization: Bearer $UNIQUICK_TOKEN" -H "Content-Type: text/html" \ --data-binary @index.html open https://uniquick.azurewebsites.net/s/-hello/ ## CLI export UNIQUICK_TOKEN=qk_... # from https://uniquick.azurewebsites.net/token npx tsx cli/uniquick.ts create my-site --title "My Site" npx tsx cli/uniquick.ts deploy ./dist --site my-site npx tsx cli/uniquick.ts list npx tsx cli/uniquick.ts delete my-site --yes npx tsx cli/uniquick.ts token-check ## MCP server (Claude Code / Claude Desktop / Codex) claude mcp add uniquick --env UNIQUICK_TOKEN=qk_... -- npx tsx /path/to/uniquick/mcp/server.ts Tools: create_site, deploy_site, list_sites, delete_site, get_site_url. deploy_site only accepts directories inside UNIQUICK_DEPLOY_ROOT (env var; default: the MCP server's working directory). Deploys skip dotfiles, symlinks, .git and node_modules, and are capped at 5MB per file / 40MB total. These limits apply to the CLI deploy command too. ## Getting a token (humans do this once) 1. Open https://uniquick.azurewebsites.net/token in a browser 2. Sign in with your @illinois.edu account 3. Create a token (label it, e.g. "claude-code laptop") — shown ONCE, copy it 4. export UNIQUICK_TOKEN=qk_... in the agent's environment Tokens are revocable from the same page. ## Runtime endpoints (the SDK calls these for you — listed for completeness) GET/PUT/DELETE /api/data/:site/:key KV (key may contain slashes) GET /api/data/:site?prefix= list keys POST /api/files/:site multipart upload -> {url} GET /f/:site/:id/:name serve an upload (no auth — capability URL, link = access) WS /ws/:site first message {"type":"auth","token":""} POST /api/ai/chat {"prompt":"...","system":"...","stream":true,"site":"my-site"} non-stream response: {"text":"...","usage":{"total_tokens":1234}} with "stream":true: SSE lines of the form data: {"token":"..."} terminated by data: [DONE] GET /api/me {name, upn} GET /api/sdk-config {clientId, tenantId, devMode} (public) Security note: runtime API calls made from a site page are pinned to that site — the server derives the originating site from the request's Referer path and rejects cross-site calls with 403 PERMISSION. This is a soft control: it stops accidental cross-site calls, but page JS can suppress the Referer (e.g. referrerPolicy "no-referrer") and fall into the unrestricted no-Referer path. Calls without a Referer (CLI, server-side scripts, curl with a deploy token) are not restricted. The same caveat applies to token minting: malicious site JS could suppress the Referer and mint a deploy token as the visitor using the visitor's Entra token from the shared-origin MSAL cache; mitigations are the interactive-credential requirement, the Referer soft check, a 20-active-token cap, and the /token page listing all active tokens for audit — the real fix is per-site subdomains (deferred). WebSocket rooms are not origin-pinned in v1.