Apps are public by default. Authentication can protect any static site, HTML app, handler tree, container service, or selected path.

policies

policy verified identity
required / app-users active app-scoped user
owner active owning Kedge account
google-domains exact signed Google Workspace hd claim
github-orgs active GitHub organization membership

Policy members are ORed. App users may sign in with email, passkey, Google, or GitHub. Workforce identities pass an owner, domain, or organization rule without creating an app-user record.

Policies cover every path by default. Selected paths use exact matches, /**, 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.

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"}

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.

HTML apps

<a data-when="!$viewer.verified" href="/_kedge/auth/login">log in</a>
<p data-when="$viewer.verified">Signed in as {{$viewer.name}}</p>

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.

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

Authentication is currently declared by the top-level x-kedge.auth property:

x-kedge:
  auth: required

A compose.yaml containing only x-kedge does not create a container service. Use owner for an owner-only app. Use the structured form for paths and workforce rules:

x-kedge:
  auth:
    paths: ["/admin/**"]
    except: ["/admin/health"]
    allow:
      owner: true
      google-domains: [example.com]
      github-orgs: [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. See the Compose reference.