Skip to content

Who is visiting: identity headers

How your App knows who is using it without any login code: Basemodo signs visitors in and sends their email, id and role with every request.

The headers

Every request reaches an App through Basemodo's gate, never directly. A new App is Private: the gate shows visitors a sign-in page and lets through only the people the App is shared with. On every request from a signed-in visitor it tells the App who they are:

  • X-Basemodo-Email: their email, lowercase.
  • X-Basemodo-Id: their Basemodo id, stable even if their email changes; key your own records on it.
  • X-Basemodo-Role: what they are to the App (owner, member, or editor for a Member the Owner also lets deploy it), when they have a role (a visitor let in by a Link, Public or Workspace Visibility has none). For an App a Workspace owns, each of its Admins is an owner.
  • X-Basemodo-Jwt: the same, signed (EdDSA) with a key of this App, for an App that wants to verify it: claims iss, aud (the App's URL), sub (the id), email, role, app, iat, exp (a few minutes). Verify it against the public key in the App's environment, BASEMODO_JWT_PUBLIC_KEY (PEM).

The gate removes every X-Basemodo-* header a client sends and never forwards its own cookies, so an App can trust these headers and must not implement login of its own. A visitor who has not signed in (on a Public App) arrives without them. Paths under /.basemodo/ belong to the gate and never reach the App.

Search engines index no App unless its Owner says so: the gate sends X-Robots-Tag: noindex, nofollow, noarchive on every answer and serves a robots.txt that disallows everything, replacing the App's own. Only for a Public App, indexing on (or [web] indexable = true) lets them in: the App's answers then leave as it sent them, and robots.txt lets crawlers in. Leaving Public turns it off.

On the App's HTML pages, the gate shows signed-in visitors Basemodo's Toolbar: a small pill with who is here now, the App's Visibility and a way to share it, which only shows (who can reach the App is changed on basemodo.com). It is the script /.basemodo/toolbar.js, same-origin, so a Content Security Policy with script-src 'self' lets it in and a nonce-only policy keeps it out. It is on for every App; toolbar off (or [web] toolbar = false) turns it off.

Reading them

The headers arrive on every request, so "who is this?" is one line. Node with Express:

app.get("/", (req, res) => {
const email = req.get("X-Basemodo-Email"); // "ana@example.com"
const id = req.get("X-Basemodo-Id"); // keep your records under this
const role = req.get("X-Basemodo-Role"); // "owner", "member", or undefined
res.send(`Hello ${email ?? "stranger"}`);
});

Python with Flask:

from flask import Flask, request
app = Flask(__name__)
@app.get("/")
def home():
email = request.headers.get("X-Basemodo-Email")
is_owner = request.headers.get("X-Basemodo-Role") == "owner"
return f"Hello {email or 'stranger'}"

Store your own data under X-Basemodo-Id rather than the email: the id stays the same if the person's email changes.

Only the owner may see a settings page? Check the role:

if (req.get("X-Basemodo-Role") !== "owner") return res.status(403).send("Owners only");

Verifying the signed token

The headers are safe to trust as they are, because every request passes through Basemodo, which removes any X-Basemodo-* header a visitor sends. If you want proof anyway (your App also runs somewhere else, or you pass the identity on to another service), verify X-Basemodo-Jwt with the public key Basemodo gives your App in BASEMODO_JWT_PUBLIC_KEY. Node, with the jose package:

import { importSPKI, jwtVerify } from "jose";
const key = await importSPKI(process.env.BASEMODO_JWT_PUBLIC_KEY, "EdDSA");
async function visitor(req) {
const token = req.get("X-Basemodo-Jwt");
if (!token) return null; // not signed in (a Public App)
const { payload } = await jwtVerify(token, key, {
issuer: "https://basemodo.com",
audience: "https://lunch-rota.basemodo.app", // your App's address
});
return { id: payload.sub, email: payload.email, role: payload.role };
}

Python, with PyJWT and cryptography:

import os
import jwt
KEY = os.environ["BASEMODO_JWT_PUBLIC_KEY"]
def visitor(headers):
token = headers.get("X-Basemodo-Jwt")
if not token:
return None
claims = jwt.decode(
token, KEY, algorithms=["EdDSA"],
issuer="https://basemodo.com",
audience="https://lunch-rota.basemodo.app",
)
return {"id": claims["sub"], "email": claims["email"], "role": claims.get("role")}

The token lives a few minutes and is for your App's address only: a token sent to one App is refused by any other.

Running your App on your own computer

When you run your App locally, nothing sets these headers. Fall back to a fixed person while developing (for example req.get("X-Basemodo-Email") ?? "me@localhost" when NODE_ENV is not production), and never in a deployed App.

Who gets in

The headers say who is visiting; whether they get in at all is the App's Visibility and its Members, set by its Owner: see Sharing. A visitor let in only because the App is Link or Public has no role. Requests to Public Paths (webhooks) carry no identity unless the caller happens to be signed in to the App.