Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone cf614aedcf4cdabfa9c45095
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
functions/api.ts
// Forms' public HTTP API: no account. Anyone can create a form and fill one in; reading responses and
// changing a form need the owner key handed back at creation. POST /forms/:id/responses doubles as a
// website form endpoint: point an HTML form's action at it. GET / describes the routes.
// Writes go through the same policy rules as the web app, as this route's public principal.
import { SITE_URL, API_URL } from "./lib/config";
import {
checkQuestions, checkFormText, parseQuestions, checkAnswers, checkFreeFields, parseSubmission, newestFirst, columns, summarize, toCsv,
randomId, QUESTION_TYPES, type FormDoc, type Question, type ResponseDoc, type Submission,
} from "./lib/forms";
const NOW = { operation: "time", value: "now" };
const ID_RE = /^[A-Za-z0-9]{8,32}$/;
const cors = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PATCH, DELETE, OPTIONS",
"access-control-allow-headers": "content-type, x-owner-key, idempotency-key",
};
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body, null, 2), { status, headers: { "content-type": "application/json; charset=utf-8", ...cors } });
const fail = (status: number, error: string, extra?: Record<string, unknown>) => json({ error, ...extra }, status);
class HttpError extends Error { constructor(public status: number, message: string) { super(message); } }
async function jsonBody(req: Request): Promise<Record<string, any>> {
try {
const parsed = await req.json();
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
} catch { /* fall through */ }
throw new HttpError(400, "Send a JSON object body.");
}
/** A submission body: JSON {answers: {...}} (or the answers object itself), or an HTML form post. */
async function submissionBody(req: Request, type: string): Promise<Record<string, unknown>> {
if (type.includes("application/x-www-form-urlencoded") || type.includes("multipart/form-data")) {
const fields: Record<string, unknown> = {};
const add = (k: string, v: string) => {
const prev = fields[k];
fields[k] = prev === undefined ? v : Array.isArray(prev) ? [...prev, v] : [prev, v];
};
if (type.includes("multipart/form-data")) {
let data: FormData;
try { data = await req.formData(); } catch { throw new HttpError(400, "The form data could not be read."); }
data.forEach((v, k) => { if (typeof v === "string") add(k, v); });
} else {
new URLSearchParams(await req.text()).forEach((v, k) => add(k, v));
}
return fields;
}
const input = await jsonBody(req);
const answers = input.answers ?? input;
if (!answers || typeof answers !== "object" || Array.isArray(answers)) throw new HttpError(400, "answers must be an object of question id or label to answer.");
return answers;
}
async function listAll(ctx: any, path: string): Promise<any[]> {
const rows: any[] = [];
let cursor: string | undefined;
for (let page = 0; page < 100; page++) {
const res = await ctx.bounded.get(path, { limit: 1000, ...(cursor ? { cursor } : {}) });
// Rows name their path in _id (or pathId); the bare id is its last segment.
for (const r of res?.data || []) rows.push({ ...r, id: r.id ?? String(r._id ?? r.pathId ?? "").split("/").pop() });
cursor = res?.nextCursor;
if (!cursor) break;
}
return rows;
}
/** Joins the form as this route (knowing the id is the permission to read it), then reads it. */
async function openForm(ctx: any, formId: string): Promise<FormDoc> {
if (!ID_RE.test(formId)) throw new HttpError(404, "No form with that id.");
// Once this route has joined, one read is enough (a refused read comes back empty, not as an error).
const joined = await ctx.bounded.get(`forms/${formId}`);
if (joined) return joined;
try {
await ctx.bounded.set(`forms/${formId}/joins/${ctx.user.id}`, { at: NOW });
} catch {
// A request that arrived at the same moment may have joined first; then the read below works.
}
const form = await ctx.bounded.get(`forms/${formId}`);
if (!form) throw new HttpError(404, "No form with that id.");
return form;
}
/** Checks the owner key, then makes this route an owner of the form (once) so it can read responses. */
async function asOwner(ctx: any, req: Request, url: URL, formId: string): Promise<FormDoc> {
const key = (req.headers.get("x-owner-key") || url.searchParams.get("key") || "").trim();
if (!key) throw new HttpError(401, "Send the form's owner key in the x-owner-key header (or as ?key=).");
const form = await openForm(ctx, formId);
const stored = await ctx.bounded.get(`formkeys/${formId}`);
if (!stored?.key || stored.key !== key) throw new HttpError(403, "That owner key does not match this form.");
const me = ctx.user.id;
if (!(await ctx.bounded.get(`forms/${formId}/owners/${me}`))) {
await ctx.bounded.setMany([
{ path: `formclaims/${formId}/c/${me}`, document: { key } },
{ path: `forms/${formId}/owners/${me}`, document: { at: NOW } },
]);
}
return form;
}
const links = (formId: string, key?: string) => ({
url: `${SITE_URL}/f/${formId}`,
endpoint: `${API_URL}/forms/${formId}/responses`,
...(key ? { adminUrl: `${SITE_URL}/f/${formId}/edit#k=${key}`, resultsUrl: `${SITE_URL}/f/${formId}/results#k=${key}` } : {}),
});
function publicForm(formId: string, form: FormDoc) {
const questions = parseQuestions(form.questions);
return {
id: formId,
title: form.title,
description: form.description || undefined,
thankYou: form.thankYou || undefined,
closed: Boolean(form.closed),
questions: questions.map((q) => ({ id: q.id, type: q.type, label: q.label, help: q.help, required: Boolean(q.required), options: q.options, showIf: q.showIf })),
...links(formId),
howToAnswer: questions.length
? `POST ${API_URL}/forms/${formId}/responses with {"answers": {"<question id or label>": value}}. Text, email, date ("2026-10-18") and choice answers are strings; number, rating (1 to 5) and scale (0 to 10) are numbers; checkboxes is a list of options; yes_no is "yes" or "no". A question with showIf is asked only when that earlier question's answer is (or, for checkboxes, includes) showIf.is; otherwise leave it out.`
: `This form has no questions: POST any fields (up to 50) to ${API_URL}/forms/${formId}/responses as JSON {"answers": {...}} or as an HTML form post.`,
};
}
/** Answers keyed by column label (labels that repeat get the question id added) for people and assistants. */
function labelled(cols: ReturnType<typeof columns>, s: Submission) {
const seen = new Set<string>();
const out: Record<string, unknown> = {};
for (const c of cols) {
const name = seen.has(c.label) ? `${c.label} (${c.key})` : c.label;
seen.add(c.label);
if (s.answers[c.key] !== undefined) out[name] = s.answers[c.key];
}
return out;
}
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!);
/** The small page a browser sees after posting a website form straight to the endpoint. */
function htmlPage(status: number, title: string, body: string, referer: string | null) {
let back = "";
try {
const u = referer ? new URL(referer) : null;
if (u && (u.protocol === "https:" || u.protocol === "http:")) back = `<a class="btn" href="${esc(u.href)}">Back to ${esc(u.host)}</a>`;
} catch { /* no link back */ }
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex"><title>${esc(title)}</title>
<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#fdfdfc;color:#15171b;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;padding:24px;box-sizing:border-box}main{max-width:520px;width:100%;background:#fff;border:1px solid #e3e6ea;border-radius:18px;padding:32px 28px;box-shadow:0 8px 24px -12px #1a1d2426}h1{margin:0 0 10px;font:400 30px/1.15 Georgia,"Times New Roman",serif;letter-spacing:-.02em}p,li{color:#4a4e56;white-space:pre-line}ul{padding-left:20px}.btn{display:inline-block;margin-top:18px;padding:12px 18px;border-radius:10px;background:#0b0b0c;color:#fff;font-weight:600;text-decoration:none}.foot{margin-top:24px;font-size:13px;color:#5d6169}.foot a{color:#2e5ec4;text-decoration:none}</style></head>
<body><main><h1>${esc(title)}</h1>${body}${back}<p class="foot">Sent with <a href="${SITE_URL}/">Forms</a>, free from OpenApps.</p></main></body></html>`;
return new Response(html, { status, headers: { "content-type": "text/html; charset=utf-8", ...cors } });
}
const routes = {
name: "Forms by OpenApps",
about: "Free forms and surveys with unlimited responses. No account. Creating a form returns an owner key: keep it to read responses and change the form.",
docs: `${SITE_URL}/llms.txt`,
openapi: `${SITE_URL}/openapi.json`,
questionTypes: Object.keys(QUESTION_TYPES),
routes: [
"POST /forms {title, description?, questions: [{type, label, required?, options?, help?, showIf?: {question, is}}], thankYou?} -> formId, url, adminUrl, resultsUrl, ownerKey, endpoint",
"GET /forms/:id -> the public form: title, questions with ids, closed",
"POST /forms/:id/responses {answers: {<question id or label>: value}} -> or an HTML form post (urlencoded or multipart) whose field names are question labels or ids",
"GET /forms/:id/responses?format=json|csv (header x-owner-key) -> every response, with a summary per question",
"PATCH /forms/:id {title?, description?, questions?, thankYou?, closed?} (header x-owner-key)",
"DELETE /forms/:id/responses/:responseId (header x-owner-key)",
],
};
export default async function api(req: Request, ctx: any) {
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
const url = new URL(req.url);
const parts = url.pathname.split("/").filter(Boolean);
// Accept both /forms/... and /api/forms/... so a copied path works either way.
if (parts[0] === "api") parts.shift();
const type = (req.headers.get("content-type") || "").toLowerCase();
const htmlPost = req.method === "POST" && parts[2] === "responses" && (type.includes("application/x-www-form-urlencoded") || type.includes("multipart/form-data"))
&& !(req.headers.get("accept") || "").includes("application/json");
const referer = req.headers.get("referer");
try {
if (!parts.length && req.method === "GET") return json(routes);
if (parts[0] !== "forms") return fail(404, "Unknown route. GET / lists the routes.");
const me = ctx.user.id;
if (parts.length === 1 && req.method === "POST") {
const input = await jsonBody(req);
const text = checkFormText(input);
if ("error" in text) return fail(400, text.error);
const checked = checkQuestions(input.questions);
if ("error" in checked) return fail(400, checked.error);
const formId = randomId(20);
const key = randomId(24);
const doc = { ...text, questions: JSON.stringify(checked.questions), closed: false };
await ctx.bounded.setMany([
{ path: `forms/${formId}`, document: { ...doc, createdBy: me, createdAt: NOW } },
{ path: `forms/${formId}/joins/${me}`, document: { at: NOW } },
{ path: `forms/${formId}/owners/${me}`, document: { at: NOW } },
{ path: `formkeys/${formId}`, document: { key } },
]);
return json({
formId, ...links(formId, key), ownerKey: key,
form: publicForm(formId, { ...doc, createdBy: me, createdAt: 0 }),
note: "Send people the url. Keep ownerKey and adminUrl private: they let anyone read the responses and change the form.",
}, 201);
}
const formId = parts[1];
if (parts.length === 2 && req.method === "GET") return json(publicForm(formId, await openForm(ctx, formId)));
if (parts.length === 2 && req.method === "PATCH") {
const form = await asOwner(ctx, req, url, formId);
const input = await jsonBody(req);
const text = checkFormText(input, form);
if ("error" in text) return fail(400, text.error);
const update: Record<string, unknown> = { ...text };
if (input.questions !== undefined) {
const checked = checkQuestions(input.questions);
if ("error" in checked) return fail(400, checked.error);
update.questions = JSON.stringify(checked.questions);
}
if (input.closed !== undefined) {
if (typeof input.closed !== "boolean") return fail(400, "closed must be true or false.");
update.closed = input.closed;
}
await ctx.bounded.set(`forms/${formId}`, update);
return json(publicForm(formId, { ...form, ...update } as FormDoc));
}
if (parts[2] === "responses" && parts.length === 3 && req.method === "POST") {
const form = await openForm(ctx, formId);
const fields = await submissionBody(req, type);
const thanks = form.thankYou || "Thanks. Your response was sent.";
if (form.closed) {
if (htmlPost) return htmlPage(403, "This form is closed", "<p>It no longer takes responses.</p>", referer);
return fail(403, "This form is closed and no longer takes responses.");
}
// A filled honeypot field means a bot: answer as if it worked and store nothing.
if (String(fields._gotcha ?? "").trim()) return htmlPost ? htmlPage(200, "Thank you", `<p>${esc(thanks)}</p>`, referer) : json({ ok: true, message: thanks }, 201);
const questions = parseQuestions(form.questions);
let answers;
let ignored: string[] = [];
if (questions.length) {
const checked = checkAnswers(questions, fields);
if (Object.keys(checked.errors).length) {
const byLabel = Object.fromEntries(Object.entries(checked.errors).map(([id, e]) => [questions.find((q) => q.id === id)?.label ?? "Form", e]));
if (htmlPost) {
const items = Object.entries(byLabel).map(([l, e]) => `<li><strong>${esc(l)}</strong>: ${esc(e)}</li>`).join("");
return htmlPage(400, "Some answers need fixing", `<p>Go back, fix these, and send the form again.</p><ul>${items}</ul>`, referer);
}
return fail(400, "Some answers need fixing.", { errors: byLabel });
}
answers = checked.answers;
ignored = checked.ignored;
} else {
const free = checkFreeFields(fields);
if ("error" in free) return htmlPost ? htmlPage(400, "That did not go through", `<p>${esc(free.error)}</p>`, referer) : fail(400, free.error);
answers = free.answers;
}
const responseId = randomId(16);
try {
await ctx.bounded.set(`forms/${formId}/responses/${responseId}`, { answers: JSON.stringify(answers), by: me, createdAt: NOW });
} catch (err) {
// The rule refuses a response the moment the form closes, even if it closed after we read it.
const closedNow = (await ctx.bounded.get(`forms/${formId}`))?.closed;
if (!closedNow) throw err;
return htmlPost ? htmlPage(403, "This form is closed", "<p>It no longer takes responses.</p>", referer) : fail(403, "This form is closed and no longer takes responses.");
}
if (htmlPost) return htmlPage(200, "Thank you", `<p>${esc(thanks)}</p>`, referer);
return json({ ok: true, responseId, message: thanks, ...(ignored.length ? { ignored, note: "These fields were not saved: they matched no question, or the other answers hide that question." } : {}) }, 201);
}
if (parts[2] === "responses" && parts.length === 3 && req.method === "GET") {
const form = await asOwner(ctx, req, url, formId);
const questions: Question[] = parseQuestions(form.questions);
const subs = (await listAll(ctx, `forms/${formId}/responses`) as ResponseDoc[]).map(parseSubmission).sort(newestFirst);
const format = (url.searchParams.get("format") || "json").toLowerCase();
if (format === "csv") {
const name = form.title.replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "responses";
return new Response(toCsv(questions, subs), {
headers: { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="${name}.csv"`, ...cors },
});
}
if (format !== "json") return fail(400, "format must be json or csv.");
const cols = columns(questions, subs);
return json({
form: publicForm(formId, form),
count: subs.length,
summary: summarize(questions, subs),
responses: subs.map((s) => ({ id: s.id, submittedAt: new Date(s.createdAt * 1000).toISOString(), answers: labelled(cols, s) })),
});
}
if (parts[2] === "responses" && parts.length === 4 && req.method === "DELETE") {
await asOwner(ctx, req, url, formId);
const path = `forms/${formId}/responses/${parts[3]}`;
if (!ID_RE.test(parts[3]) || !(await ctx.bounded.get(path))) return fail(404, "No response with that id in this form.");
await ctx.bounded.set(path, null);
return json({ ok: true, deleted: parts[3] });
}
return fail(404, "Unknown route. GET / lists the routes.");
} catch (err) {
const status = err instanceof HttpError ? err.status : 500;
const message = err instanceof HttpError ? err.message : `Something went wrong: ${err instanceof Error ? err.message : String(err)}`;
if (htmlPost) return htmlPage(status, "That did not go through", `<p>${esc(message)}</p>`, referer);
return fail(status, message);
}
}
Change history
Quality pass: conditional questions, 0 to 10 scale, QR code, prefill links, results tabs, API docs
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync