brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
restricted-accessUse when a val's HTTP endpoints should not be open to the whole internet — limiting an app to a team, understanding why an endpoint redirects to a login page, letting a webhook through, or identifying which Val Town user is viewing an app. Covers app access (`httpPrivacy`), org grants, bypass tokens for automation, and the `X-Val-Town-User` identity header.…
Independent GitHub snapshot · Automated classification · Original documentation governs
httpPrivacy), org grants, bypass tokens for automation, and the X-Val-Town-User identity header. For building your own login flow inside a val, see the oauth skill instead.
triggers: [restricted, access, private, permission, grant, allowed, team, internal, intranet, bypass, token, viewer, identity, login, redirect, "302", gated, unauthorized, forbidden]A val has two independent access settings. Changing one does not change the other:
privacy: public / unlisted / private) — who can read the source on val.town.httpPrivacy: public / restricted) — who can call the val's HTTP endpoints.A val can have private code and a wide-open endpoint, or public code and a locked-down endpoint. update_val's privacy field only moves the first one; app access is changed with set_http_privacy.
Restricted app access is available to organizations that have the feature enabled. Vals created in such an org may default to restricted — always read httpPrivacy off a get_val_detail, list_vals, create_val, or remix_val response rather than assuming a new val's URL is open.
Two different things both sound like "make my app require a login":
std/oauth (see the oauth skill) runs inside your val: you wrap your handler, and anyone with a Val Town account can log in. You control the session and can build per-user features.Pick restricted access for an internal tool that only your team should reach. Pick std/oauth when any Val Town user may sign in and the app needs its own notion of a logged-in user.
Don't stack them by accident. Adding oauthMiddleware to an already-restricted val means the visitor authenticates twice — once at the gate, once in your code. If a restricted val needs to know who is viewing, use the identity header below instead of adding OAuth.
Access is granted to organizations, not individual people. A viewer gets in when the val has a grant to an org and that viewer is a member of it. Removing either one revokes access on the very next request — nothing is cached for the length of a session.
Grants come from:
add_allowed_user grants an org, list_allowed_users shows current grants, remove_allowed_user revokes one.list_allowed_users alongside direct grants.An unauthenticated request does not reach the val. The platform answers with a 302 redirect to a Val Town login or authorization page. This is the single most common source of confusion when debugging a restricted val:
fetch_val_endpoint reports a redirect it won't follow.curl shows a 302 to val.town instead of your response.403 explaining they need access to their organization.None of these mean the val's code is broken. Check httpPrivacy first — if it's restricted, the gate is doing its job. Make the val public with set_http_privacy, grant the caller's org, or use a bypass token.
Machines can't complete a login redirect, so a restricted val that receives webhooks (Stripe, GitHub, a cron job in another val) needs a bypass token — a secret scoped to that one val.
Create it with create_bypass_token; the secret is shown once and cannot be retrieved again. Manage tokens with list_bypass_tokens and revoke_bypass_token.
Present it either way:
// Header (preferred — keeps the secret out of logs and referrers)
await fetch(url, { headers: { "X-Val-Town-Access": Deno.env.get("MY_BYPASS_TOKEN")! } });
// Query param (for services that only accept a URL, e.g. some webhook configs)
await fetch(`${url}?val_town_access=${Deno.env.get("MY_BYPASS_TOKEN")}`);
The platform strips the header and the query param before your handler runs, so your code never sees them. A bypass-token request carries no viewer identity — it is an anonymous machine caller.
For a human viewer who came in through the gate, the platform forwards a short-lived signed X-Val-Town-User header. It is not the identity itself — exchange it for the viewer's profile using the val's own API token, which Val Town injects as the valtown environment variable:
const IDENTITY_HEADER = "X-Val-Town-User";
/** Returns the viewer's public profile, or null when there isn't one. */
async function getViewer(req: Request) {
const signed = req.headers.get(IDENTITY_HEADER);
if (!signed) return null;
const res = await fetch("https://api.val.town/v3/val/viewer", {
headers: {
Authorization: `Bearer ${Deno.env.get("valtown")}`,
[IDENTITY_HEADER]: signed,
},
});
if (!res.ok) return null;
// { id, username, type, bio, profileImageUrl, url, links }
return await res.json();
}
Rules that matter:
!-assert it or index into a null result.The transport above (X-Val-Town-User plus the /v3/val/viewer exchange) is how this works today and may change; the three rules hold regardless.
| Task | Tool |
|---|---|
| Check the current setting | get_val_detail (httpPrivacy field) |
| Make an endpoint public or restricted | set_http_privacy |
| See who has access | list_allowed_users |
| Grant / revoke an org | add_allowed_user / remove_allowed_user |
| Create / list / revoke automation secrets | create_bypass_token / list_bypass_tokens / revoke_bypass_token |
Restricted vals can only be iframed by val.town, so an embed of one on an external site will be blocked by the browser regardless of who's logged in.