Ralvapass · accounts for every app you run

A verifier can check a token. It can never make one.

Ralvapass signs tokens with a private key and publishes only the public one. Every other service checks them locally, so a stolen verifier gives an attacker nothing to mint with.

Forge a token

1 · Issuerholds the private key

Ralvapass. The only place a token is made.

private key
kept by the service, never published
2 · On the wiresample token shortened
eyJhbGciOiJFZERTQSIs….eyJpc3MiOiJpZGVudGl0eSIs….kQe3…
// header and payload, decoded
{ "alg": "EdDSA", "kid": "sample" }
{ "iss": "identity", "sub": "usr_sample", "aud": ["app-a"], "exp": … }

What an attacker without the private key gets

  • Nothing changedAccepted
  • Audience changed to app-bRejected: signature invalid
  • Expiry extended by a dayRejected: signature invalid
  • One bit of the signature flippedRejected: signature invalid
  • Signed legitimately for app-bRejected: audience is not app-a
3 · Verifierapp-a’s backend · public key only
public key (from /.well-known/jwks.json)
{"kty":"OKP","crv":"Ed25519","kid":"sample","x":"…"}
  • Signature matches the public key
  • Issuer is identity
  • Not expired
  • Audience is app-a

Accepted. The verifier never called the issuer.

The service signs with EdDSA and a kid, and a verifier fetches the public key from the JWKS (cacheable for 300 s). The issuer identity and the 15-minute lifetime are the service’s defaults; the keys, the application codes and the subject are sample.

The spec

One identity service, and every figure counted from its source.

  • 8services

    RPC services

    7 for accounts, tokens and management, 1 for the operator console. Served over Connect and native gRPC.

    real contracts v0.33.0 · ^service

  • 91RPCs

    Calls in the contract

    68 across the 7 account and management services, 23 in the operator console’s.

    real contracts v0.33.0 · ^\s*rpc

  • 3transports

    One port, three doors

    Connect, native gRPC and REST, all over the same service. One conformance suite runs every scenario on each.

    real identity/transport/*

  • 55migrations

    In the identity library

    The service wraps the library and runs its migrations at startup. It adds no tables of its own for pools.

    real identity v3.3.0 · migrations/*.sql

  • 15min

    Access-token lifetime

    The default. Verifiers check the signature locally, so a signed-in user carries on if the service is briefly down.

    real defaultAccessTTL

  • 300s

    Public-key cache

    The JWKS is served with a five-minute cache, and verifiers fetch it only for a key id they have not seen.

    real jwksCacheControl

  • 14REST paths

    Public REST surface

    The surface for browsers, the CLI and personal access tokens, described by one OpenAPI file.

    real contracts v0.33.0 · paths:

  • 12releases

    Tagged so far

    The latest is v0.5.0-rc.1. Releases are cut by the owner, never by CI alone.

    real git tag

Every cell was counted from the source when this page was built. The build compares them with what the copy says and stops if they differ.

How it is counted

If the code changes, the page refuses to build.

A script reads the contracts and the library, writes the figures to a file that git ignores, and compares them with what the page claims. A drift is a failed build, not a stale number.

gen-data · build outputsample run
$ pnpm build
gen-data counting from contracts v0.33.0
  services   8   page says 8   ok
  rpcs       91  page says 91  ok
  migrations 55  page says 55  ok
after a new proto lands, with the page unchanged:
  rpcs       93  page says 91  drift
build failed: 1 claim no longer matches the source

How apps trust it

One token, checked everywhere, minted in one place.

A service gets its own token from Ralvapass, and every other service verifies it with public keys. Scroll and the diagram follows.

  1. A service asks for a token

    Its backend authenticates as a service account: POST /oauth/token with grant_type=client_credentials, credentials by HTTP Basic or in the form body. No secret is shared with any other service.

  2. Ralvapass signs one

    An EdDSA access token. The header names the signing key (kid); the audience is the application’s code, app-a; the lifetime is 15 minutes by default.

  3. The service calls app-a

    It sends the token as a bearer. app-a’s backend verifies it locally, with a public key: it never asks Ralvapass whether the caller is allowed.

  4. Only for a key it has not seen, it fetches the JWKS

    GET /.well-known/jwks.json, served with a 300-second cache. A verifier holds public keys only, so it can check a token and cannot make one.

  5. app-a accepts the call

    Signature, issuer, expiry and audience all pass, so the request goes through. This is the figure above, with the wire drawn.

Sequencesample key id and service name

A service gets a token from Ralvapass and calls app-a, which verifies it locally. The five steps are listed beside this diagram.

Pools and applications

Who can sign in where is one setting, not three codebases.

A pool is a set of users sharing credentials and an email namespace. Each application selects exactly one pool. The three layouts below are the same mechanism.

  • Layout 1Isolated
    Isolatedpool-apool-bpool-capp-aapp-bapp-c

    Isolated

    A pool selected by exactly one application. Each app keeps its own people.

  • Layout 2Shared group
    Shared groupsharedpool-capp-aapp-bapp-c

    Shared group

    Two applications select one pool, so they share accounts; a third keeps its own.

  • Layout 3One global pool
    One global poolglobalapp-aapp-bapp-c

    One global pool

    Every application selects the same pool. Optional; nothing special about it.

  1. 01

    One pool per application, as a hard rule.Two pools behind one login would need an email-ambiguity rule. If two groups must reach one app, they share a pool.

  2. 02

    Email is unique within a pool.So an application plus an email resolves to at most one user. The same address in two pools is two separate accounts.

  3. 03

    Being in the pool is not access.An application can be open to its pool or invite-only, with a membership for each user.

  4. 04

    Repointing is explicit.Moving an application to another pool is an operation of its own, and it drops the application’s memberships.

The rules are the service’s. Pool and application names are sample.

What the token says

Roles stay in the service. They never ride in the token.

A token says who you are and which application it is for. What you may do is looked up server-side, so a role change takes effect without waiting for a token to expire.

  1. sub · uid · aud

    Who, and for which application. An application that sees an aud other than its own rejects the token. Try “Change the audience” above.

  2. iss · iat · exp · jti

    Who signed it, and for how long. The issuer is identity by default and the lifetime 15 minutes.

  3. roles and permissions

    Resolved server-side, never embedded. Each application has its own role catalog; backends ask the access service who holds what.

  4. resource rules

    Stay in your application. The service never learns what a profile or an invoice is.

/.well-known/jwks.json · what a verifier downloadssample key
{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "kid": "sample", "x": "…" } ] }

If something goes wrong

Pull one thing. See exactly what moves.

Central accounts are only worth it if the damage is predictable. Pick an event and read what each part does next. Application names are sample.

What happens?
An access token is stolenA user’s access token is copied.
  • ralvapassuntouched

    Nothing to change: the token was signed correctly.

  • app-ausable

    Accepts it until it expires, 15 minutes by default.

  • app-buntouched

    Rejects it: the audience is app-a.

It works until it expires (15 minutes by default) and only against the application it names. An optional revocation list can end it sooner.

The same events, written out.
If this happensWhat follows
app-a’s backend is breachedA verifier holds public keys only, so a breach there cannot mint a token.
An access token is stolenIt works until it expires (15 minutes by default) and only against the application it names. An optional revocation list can end it sooner.
A service-account secret leaksRevoke the account and the next call fails: it is looked up again on every call. Until then the damage stays inside its own pool.
The database is readA service-account secret is shown once and kept as a digest, so what is stored cannot be replayed as a credential.
Ralvapass is downNobody new signs in, but signed-in users carry on until their token expires (15 minutes by default): verifiers check against cached public keys.

The trade-off, said plainly

One more service that everything depends on.

You trade a copy of accounts in every app for one dependency. If Ralvapass is down, nobody new signs in or refreshes. That is the cost, and here is what limits it.

  • 0

    new sign-ins while it is down

    No sign-in, no refresh and no new service tokens until it is back.

  • 15min

    issued tokens keep working

    Verifiers check against cached public keys, so signed-in users carry on until their access token expires (the default lifetime).

  • 1

    more service to run

    With its own database, and 55 migrations applied at startup.

In return you stop carrying a copy of users, passwords and sessions in every app, a shared signing secret, and one role catalog per application kept in sync by hand.

Surfaces

Three doors, one service.

ConnectHTTP

Browsers and typed clients

Connect, gRPC and gRPC-Web from one handler.

gRPCHTTP/2

Service to service

Native gRPC from the same proto, built and tested from day one as insurance.

RESTHTTP

The public identity surface

Errors are RFC 9457 problem+json. The token endpoint speaks plain OAuth.

A behaviour that passes over one door and fails over another fails the build.

Questions

Short answers.

Do browsers call it?

Yes. REST is the public surface for browsers, the command line and personal access tokens. Backends use the RPC doors with a service account.

What happens when the service is down?

Users already signed in carry on: verifiers check the signature locally and cache the public key. Their access tokens last 15 minutes by default, after which refresh and new sign-ins wait for the service.

Can two applications share an account?

Yes, by selecting the same pool. Accounts belong to a pool, and an application selects exactly one.

Where is the code?

In a private repository. The figures on this page are counted from it, and the contracts and the identity library it uses, when the page is built.