Quick — internal Miovision sites
A site is a folder with index.html, deployed to https://<site>.quick.mio.vision. No backend, build step, or API keys — every server capability lives on the global quick object, loaded with one <script> tag (see [SDK](#sdk--global-quick)).
Okta gates the site: every visitor is a verified @miovision.com employee. No per-site permissions — the login wall is the only access control. So:
- Never store secrets or customer PII. Any employee can read all of it.
- Authenticated ≠ authorized: treat all stored data as org-readable.
- Not for: public sites, non-employees, PII/secrets.
Rules
- Static files only, served as-is. Prefer a single
index.html with vanilla JS or CDN ESM.
- Compose behavior from
quick.*. Don't write a server.
Frontend stack
No build step — use CDN libraries. Defaults:
Deploy & fetch
The quick CLI ships a folder to <site>.quick.mio.vision and pulls it back. Both auth via Miovision SSO: first run opens a browser, then caches the token (~/.config/quick/) and reuses it; expired sessions re-login automatically. One login covers every site (domain-scoped cookie). Both need the Miovision network/VPN.
quick deploy [src] [site] [--to <folder>] [--tags <a,b>]
Uploads src (default .) to site (default: dir's base name). A directory is walked recursively, preserving relative paths as served URLs; a single file is served under its base name. --to prefixes a server-side folder onto every served path.
quick deploy # cwd -> <folder-name>.quick.mio.vision
quick deploy . lunch-vote # cwd -> lunch-vote.quick.mio.vision
quick deploy ./dist lunch-vote # ./dist -> lunch-vote.quick.mio.vision
quick deploy ./dist --site lunch-vote # same; --site wins over positional
quick deploy script.js newsite --to dist # one file -> newsite/dist/script.js
quick deploy ./dist newsite --to assets # ./dist/* -> newsite/assets/*
quick deploy . lunch-vote --tags food,voting # deploy + tag (gallery filter labels)
- Subdomain comes from the second positional or
--site; the flag wins if both given.
- A single-file
src requires an explicit site (its own name can't double as the subdomain).
- Deploys are additive (the server only writes the files you send), so deploying one file into a folder leaves the rest of the site untouched.
--tags sets the site's gallery labels (comma-separated slugs: a-z 0-9 -, max 32 chars each). Omitting the flag leaves existing tags untouched; tags can also be edited later in the gallery or via quick.sites.setTags.
- Payload caps at 64 MiB. Over that, split the site or push large assets out of band (e.g.
aws s3 sync).
quick fetch <site> [dir]
Inverse: downloads site's files into dir (default: the site name). Additive — overwrites fetched files, leaves unrelated local files alone.
quick fetch lunch-vote # -> ./lunch-vote
quick fetch lunch-vote ./dist # -> ./dist
deploy and fetch accept --domain <domain> (or QUICK_DOMAIN env) to target a non-prod deployment; defaults to quick.mio.vision.
quick update
Replaces the running quick binary in place with the latest build from get.quick.mio.vision (same source as the curl installer), then re-runs quick init to refresh the agent skill so the two always move together. Detects your OS/arch automatically; pass --base <url> to pull from a different host. Only works on the installed binary — running under node/bun (dev) is refused.
quick update # self-update to the latest binary (+ refresh skill)
quick --version # print the installed version
Every command also does a quick background version check and prints a one-line notice to stderr if the binary is behind (it never touches stdout or fails the command): quick: a newer version is available … Run 'quick update' to upgrade. If you see it, run quick update before continuing — that upgrades the binary and refreshes this skill in one step. Set QUICKNOUPDATE_CHECK=1 to silence the check (e.g. pinned CI).
SDK — global quick
Every page's <head>:
<link rel="icon" href="/__quick/favicon.svg" type="image/svg+xml" />
<script src="/__quick/quick-sdk.js"></script>
Both are platform /__quick/ assets served by the server. The <script> is required — quick.* attaches to the global once it loads (no import/bundler). The <link> is the shared Quick "Q" tab mark; include verbatim, don't ship a per-site favicon.ico (a site that needs its own icon still can).
Identity
const me = await quick.me(); // { userId, email, name }
Database — quick.db
Schemaless JSON docs, scoped per site (Postgres JSONB).
const posts = quick.db.collection("posts");
const doc = await posts.create({ title: "Hello" }); // { id, title }
await posts.get(doc.id);
await posts.update(doc.id, { title: "Edited" }); // shallow-merge; other fields kept
await posts.list(); // [{ id, ... }]
await posts.delete(doc.id);
Cross-site reads — collection(name, { site })
A collection is scoped to this page's site by default. Pass { site } to read/write another site's collection (e.g. a db inspector). Fine because all Quick data is org-readable — it's not a security boundary. subscribe() is NOT supported cross-site (the live feed rides this page's socket) — it throws; use list()/polling.
const theirs = quick.db.collection("posts", { site: "lunch-vote" });
await theirs.list();
Collections aren't enumerable — they exist only where docs were written, no "list collections" API. List sites with quick.sites.list(); the collection name must be known.
Queries — chained where
Sugar over list() + client-side filter: the whole collection is fetched then filtered in the browser, so use on reasonably-sized collections. Constraints AND together; a field may be constrained more than once. Chains are immutable.
const r = await quick.db.collection("employees")
.where("salary", ">", 100000)
.where("experience", ">", 0)
.get(); // [{ id, ... }]
Operators: ==, !=, >, >=, <, <=, in (value in array), contains (substring).
Realtime — subscribe
Delivers only changes after you attach — seed with list() first, then subscribe. Returns an unsubscribe fn. Optional where restricts changes (matched client-side).
Your own writes echo back. subscribe fires for every write on the site, including this tab's — so a create() you awaited also arrives as onCreate. Combined with optimistic insert, that's a double-add. Don't guard with "add only if absent" (the echo can race your create()). Funnel every path through an upsert-by-id so the doc lands once.
let docs = await posts.list(); // seed, then go live
// Insert-or-replace by id — single funnel for local writes AND echoes.
function upsert(doc) {
const i = docs.findIndex((d) => d.id === doc.id);
if (i === -1) docs.push(doc); else docs[i] = doc;
render();
}
async function add(title) { upsert(await posts.create({ title })); } // optimistic; echo re-upserts same id
const stop = posts.subscribe(
{ status: "published" }, // optional where; omit for every change
{
onCreate: (doc) => upsert(doc),
onUpdate: (doc) => upsert(doc),
onDelete: (id) => { docs = docs.filter((d) => d.id !== id); render(); },
},
);
// later: stop();
where takes a bare value for equality ({ status: "published" }) or an operator object ({ views: { gte: 100 } }; ops: eq, ne, gt, gte, lt, lte, in, contains). Chained query subscribes too: posts.where(...).subscribe({ onCreate, ... }). onDelete can't see the deleted doc's fields, so it fires for every delete regardless of where — reconcile by id.
A write notifies every page on the site (including the writer's), so plain CRUD is collaborative for free. Use quick.socket only for ephemeral data.
Sockets — quick.socket
Site-scoped broadcast for ephemeral data (cursors, presence, typing, WebRTC signaling). send reaches every other page on the site (never your own tab, never another site). Shared connection, lazy-opened, auto-reconnects. Caps: 100 concurrent connections, 50 msg/sec per connection — coalesce high-frequency events.
quick.socket.send({ type: "cursor", x, y, user: me.email });
const stop = quick.socket.onMessage((data) => {
if (data.type === "cursor") drawCursor(data);
});
// later: stop(); — or quick.socket.close() to tear down.
Fire-and-forget, never saved. For stored/replayable data use quick.db + subscribe.
AI — quick.ai
Claude via Bedrock, proxied server-side (no API key in page). Vercel AI SDK call shape: pass prompt (one turn) or messages (conversation), plus optional model (Bedrock id) and generation settings.
const { text } = await quick.ai.generateText({ prompt: "Summarize my tasks" });
// Streaming (browser JS — no `process`). `stream` yields ordered parts: text
// deltas, plus tool-call / tool-result parts when you pass `tools` (render those
// inline to show the agent's work). With tools, `files` resolves at end-of-stream
// to the changes the model proposed (ready for quick.sites.upload).
const { stream } = quick.ai.streamText({
messages: [{ role: "user", content: "Summarize my tasks" }],
});
for await (const part of stream) {
if (part.type === "text") output.textContent += part.text;
else if (part.type === "tool-call") console.log("tool:", part.name, part.args);
else if (part.type === "tool-result") console.log("→", part.id, part.result ?? part.error);
}
// Cancel a generation with an `abortSignal` (a "stop" button), same as the AI
// SDK. Aborting ends the stream (the loop throws an AbortError) and tells the
// server to stop the model — so you stop paying for tokens too. Works the same
// on generateText (it rejects with an AbortError).
const ac = new AbortController();
stopButton.onclick = () => ac.abort();
const { stream } = quick.ai.streamText({ prompt: "Write a long essay", abortSignal: ac.signal });
try {
for await (const part of stream) {
if (part.type === "text") output.textContent += part.text;
}
} catch (err) {
if (err.name !== "AbortError") throw err; // ignore the user's own cancel
}
Snowflake (v1) — quick.snow
Read-only SELECT over internally-public data via a shared service account (same data for every user, no row-level identity). rows are positional arrays aligned to columns — zip them:
const { columns, rows } = await quick.snow.sql({
query: "SELECT name, count FROM my_table LIMIT 10",
});
const objs = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
AWS (read-only) — quick.aws
Read-only AWS, run with the platform's own AWS identity (no browser credentials, like quick.snow). For internal ops/cost dashboards. Each service exposes a generic command(Name, input) over a server allowlist of read-only commands, plus typed sugar for the common ones. Inputs/outputs are the AWS SDK's own JSON (PascalCase), passed through unchanged — match the AWS API docs. Pagination is the caller's job (loop NextToken); one call = one AWS command. A command off the allowlist is rejected; nothing here can write.
// Cost Explorer — billed $ (global service).
const ce = await quick.aws.costexplorer.getCostAndUsage({
TimePeriod: { Start: "2026-06-01", End: "2026-06-22" },
Granularity: "DAILY",
Metrics: ["UnblendedCost"],
GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
});
// CloudWatch — metrics.
const m = await quick.aws.cloudwatch.listMetrics({ Namespace: "AWS/Bedrock" });
await quick.aws.cloudwatch.getMetricData({ MetricDataQueries, StartTime, EndTime });
// CloudTrail — management events (rate-limited; page via NextToken).
await quick.aws.cloudtrail.lookupEvents({
LookupAttributes: [{ AttributeKey: "EventSource", AttributeValue: "bedrock.amazonaws.com" }],
StartTime, EndTime, MaxResults: 50,
});
// Escape hatch: any allowlisted command by name.
await quick.aws.costexplorer.command("GetDimensionValues", { /* … */ });
Allowlisted today: costexplorer (GetCostAndUsage, GetDimensionValues), cloudwatch (ListMetrics, GetMetricData), cloudtrail (LookupEvents). A 501 means the surface isn't enabled on this server. Stays a stub until QUICKAWSENABLED=true and the role grants the reads.
Runtime files — quick.files
Write files into this site at runtime (e.g. drag-drop publish). The file lands beside deployed assets (siblings untouched), served as a static asset at that path. Site-scoped.
await quick.files.upload("dist/2.0.0/quick-voice.js", file); // served at /dist/2.0.0/quick-voice.js
const files = await quick.files.list(); // [{ path, size }, …]
await quick.files.delete("dist/2.0.0/quick-voice.js");
dropZone.addEventListener("drop", async (e) => {
e.preventDefault();
for (const file of e.dataTransfer.files) {
await quick.files.upload(file.name, file);
}
});
Integrations — quick.integrations
Read the current user's own external SaaS accounts (Jira, Confluence, Slack, Google Drive, Salesforce, GitLab), proxied server-side. The user links these once in WorkforceAI; Quick looks up their stored token and makes the upstream call on their behalf. The token never reaches the browser — pages only ever call the same-origin quick.* methods.
Key properties:
- Per-user, not shared. Every call acts as whoever opened the page, using their connection. A different visitor gets their own data (or nothing, if they haven't linked that provider). This is unlike
quick.snow, which is one shared service account for everyone.
- Read-only. Only GET-style reads are proxied.
- No per-site grants. Any site the user opens may use their connections.
// What has THIS user connected? Metadata only — never tokens.
const conns = await quick.integrations.list();
// [{ key: "jira", label, metadata, expiresAt, hasRefreshToken }, …]
Each provider has a small typed surface plus a generic get(path, query) escape hatch for any read on that provider's API. Put query params in the query object, not the path.
// Jira — the authed Atlassian user, then any Jira REST read.
const who = await quick.integrations.jira.myself();
const issues = await quick.integrations.jira.get("rest/api/3/search", {
jql: "assignee = currentUser() ORDER BY updated DESC",
maxResults: 20,
});
// Confluence
await quick.integrations.confluence.currentUser();
await quick.integrations.confluence.get("rest/api/space");
// Slack — the user's PERSONAL Slack (acts as them, not an org bot).
await quick.integrations.slack.authTest();
await quick.integrations.slack.users({ limit: 50, cursor }); // users.list, paginated
await quick.integrations.slack.get("api/conversations.list");
// Google Drive (Google Workspace integration, Drive-scoped)
await quick.integrations.drive.about({ fields: "user" });
await quick.integrations.drive.files({ q: "name contains 'report'" });
await quick.integrations.drive.get("drive/v3/files");
// Salesforce
await quick.integrations.salesforce.versions();
await quick.integrations.salesforce.get("services/data/v59.0/sobjects/Account");
// GitLab — the user's PERSONAL gitlab.com account (acts as them).
await quick.integrations.gitlab.currentUser(); // GET /api/v4/user
await quick.integrations.gitlab.projects({ per_page: 20 }); // projects they belong to
await quick.integrations.gitlab.get("api/v4/merge_requests", { scope: "assigned_to_me" });
A call throws if the user hasn't linked that provider — gate on quick.integrations.list() (or catch) and prompt them to connect in WorkforceAI rather than assuming a connection exists.
Multi-site — quick.sites
List the sites on Quick and deploy files to a named site from the browser (the same primitive the CLI's quick deploy uses, exposed for tools like the gallery or an in-page deployer). Unlike quick.files, which always targets the current site, sites.upload writes to any site by name.
const sites = await quick.sites.list();
// [{ name, fileCount, author, updatedBy, tags: string[], updatedAt }, …]
// author = who first deployed (never changes); updatedBy = who last deployed.
// Both are { email, name } | null.
// Deploy files to another site. Each file's `contentBase64` is its bytes,
// base64-encoded; `path` is the served URL within that site. Additive — only
// the files you send are written, siblings untouched.
await quick.sites.upload("lunch-vote", [
{ path: "index.html", contentBase64: btoa("<h1>Hi</h1>") },
]);
// Replace a site's tags (the gallery's filter labels) without redeploying.
// Tags are slugs (a-z 0-9 -, max 32 chars); the reply is the normalized,
// deduped list as stored.
await quick.sites.setTags("lunch-vote", ["food", "voting"]);