Apps are public by default. Authentication can protect any static site, HTML
app, handler tree, container service, or selected path. Declare a policy in
Compose x-kedge, an HTML meta tag, or Markdown front matter; the same
fields work in all three.
common policies
Only you:
x-kedge:
auth: owner
People you invite, each emailed a reusable sign-in link:
x-kedge:
auth:
emails: [friend@example.com]
A GitHub organization, gating one HTML page:
<meta name="kedge-auth-github" content="my-org">
policies
| field | admitted identity |
|---|---|
required / app-users: true |
active app-scoped user |
emails: [friend@example.com] |
exact verified mailbox as an app user |
owner |
active owning Kedge account |
google: true |
any Google identity |
google: [person@gmail.com] |
exact verified Google account |
google: [example.com] |
exact signed Google Workspace hd claim |
github: true |
any GitHub identity |
github: [octocat] |
exact GitHub login or active organization membership |
Policy members are ORed. Provider selectors stay flat: a Google mailbox such as
person@gmail.com is an account, while example.com (or the legacy
@example.com spelling) is a hosted domain. A GitHub value matches the
authenticated login when equal and otherwise checks active organization
membership.
emails is provider-independent. A matching email link, or the same verified
address returned by Google or GitHub, creates an app user. Provider-specific
Google accounts and GitHub accounts or organizations remain workforce
identities and do not create an app-user record.
auth gates requests. identity leaves the page public and resolves an
optional viewer for $verified, $viewer, and authored data. All declarations
in one app must admit the same identities.
Compose auth covers every path by default. Auth declared in one HTML or
Markdown page defaults to that page's resolved route. Explicit paths use exact
matches, parameterized page routes, /**, or /prefix/**, with optional
exceptions.
An email suffix or public organization listing is not sufficient. Invalid patterns, identities, fields, or an empty policy fail the deploy. Private GitHub organization membership requires Kedge's verifier to have member access to that organization. Organization owners control that access.
request behavior
Protected browser navigations receive a 303 to
/_kedge/auth/login. Protected fetches receive status 401:
{"error":"authentication_required","login_url":"/_kedge/auth/login?next=%2Fadmin"}
The login route always shows the available methods.
Login, denied, and authenticated dynamic responses are private, no-store.
Sessions are scoped to one app and exact hostname. App-user sessions have a
30-day absolute and seven-day idle lifetime; workforce sessions last eight
hours.
email invitations
After a successful deploy has a URL, Kedge emails each newly added emails
recipient. An unchanged recipient is not emailed again on later deploys. The
link can be reused while the address remains in the policy.
The login page also accepts an allowed address and sends another reusable link. The response is the same for matching and non-matching addresses. Requests are limited per address and source. A new request does not invalidate older links.
The token is carried in the URL fragment. The app-origin sign-in page removes the fragment, redeems it with a same-origin request, sets the app session, and opens the requested page. A fragment-free GET cannot redeem the token. Removing a recipient revokes its outstanding links and app-user sessions.
Invitation and sign-in delivery uses the configured transactional email sender. Pending messages are stored durably and retried by one fleet leader.
HTML apps
Gate only the page containing this meta element:
<meta name="kedge-auth" content="github">
<h1>Members</h1>
A gated page's generated reads, writes, actions, and live updates also require its admitted viewer.
Use identity when the page itself stays public. This guestbook can be read by
anyone, but only GitHub users can post:
<meta name="kedge-identity" content="github">
<a data-when="!$viewer.verified" href="/_kedge/auth/login">sign in to post</a>
<form data-kedge="posts?$verified">
<textarea name="body" required></textarea>
<button>post</button>
</form>
<ol data-kedge="posts"><template><li>{{body}}</li></template></ol>
Expanded meta properties do not require embedded JSON:
<meta name="kedge-auth-owner" content="true">
<meta name="kedge-auth-emails" content="friend@example.com, teammate@example.net">
<meta name="kedge-auth-github" content="my-org, another-org">
<meta name="kedge-auth-google" content="person@gmail.com, example.com">
<meta name="kedge-auth-paths" content="/admin/**">
<meta name="kedge-auth-except" content="/admin/health">
Provider lists are trimmed, lowercased, and deduplicated. true admits any
identity from that provider. Unknown properties, false, and empty values fail
the deploy. identity supports the same identity properties but not paths or
except.
HTML apps expose {{$viewer.id}}, name, verified, and owner.
$verified and $owner restrict individual bindings. Markdown's :account
directive supplies login, identity, and logout controls.
GET /_kedge/auth/me returns {"user":null} or id, name, email, and
provider.
Ingress admission and HTML data ownership are separate. Protecting /admin
does not make its collections owner-only. See the
HTML app reference.
handlers
Handlers receive verified identity as environment variables:
#!/usr/bin/env bash
printf 'Content-Type: application/json\r\n\r\n'
jq -n --arg id "$KEDGE_AUTH_SUBJECT" --arg email "$KEDGE_AUTH_EMAIL" \
'{id: $id, email: $email}'
The complete set is KEDGE_AUTH_SUBJECT, KEDGE_AUTH_EMAIL,
KEDGE_AUTH_PROVIDER, and KEDGE_AUTH_ASSERTION.
A runtime that serves requests concurrently keeps per-request identity off the
shared process environment. The JavaScript and TypeScript handler(req) shapes
therefore read the same identity from req.headers:
module.exports = (req) => ({id: req.headers["x-kedge-auth-subject"] || null});
services
Container services receive:
X-Kedge-Auth-Subject: <stable identity>
X-Kedge-Auth-Email: person@example.com
X-Kedge-Auth-Provider: passkey | email | kedge | google | github
X-Kedge-Auth-Assertion: <short-lived Ed25519 JWS>
Kedge strips client-supplied X-Kedge-* headers and auth cookies before adding
these headers. Verify the assertion when identity crosses another internal hop.
The verification key is at GET /_kedge/auth/jwks.json.
app-user operations
The owner-authenticated REST API lists, enables, and disables app users, reports configured providers, and revokes app sessions. Disabling a user revokes that user's sessions immediately.
Authentication is ingress authorization, not a role system. Enforce operation-level authorization in HTML binding audiences or application code.
Compose configuration
Compose uses the same shorthands and flat structure:
x-kedge:
auth: github
A compose.yaml containing only x-kedge does not create a container service.
Use the structured form to combine identities or select paths:
x-kedge:
auth:
paths: ["/admin/**"]
except: ["/admin/health"]
owner: true
emails: [friend@example.com]
google: [person@gmail.com, example.com]
github: [octocat, my-workspace]
paths defaults to ["/**"]; except wins. Top-level auth is the default for
every Compose service. A service-level x-kedge.auth replaces it.
Optional identity resolution is also available without an ingress gate:
x-kedge:
identity:
github: true
Markdown front matter uses that same structure. With no paths, inline auth
applies only to /members:
---
auth:
github: [my-org]
---
See the Compose reference.