Architecture — Versola Docs
/docs
Pre-1.00.7.5versola.ioGitHub

Architecture

The three Versola planes (central, auth, edge), the shared Postgres database, the ports, and how a browser or mobile request moves through them.

Versola is three services, grouped into three planes, plus a proxy that routes traffic and serves the admin console. This page explains what each plane owns and how a request moves through them. It does not cover deployment: see Operations.

The three planes

PlaneServiceOwns
ControlcentralThe source of truth for configuration: tenants, clients, scopes, roles, permissions, resources, signing keys. Exposes the Central API
DataauthThe OAuth 2.0 / OpenID Connect server: login, tokens, sessions, users and their credentials
TrafficedgeAn authentication and authorization reverse proxy in front of your APIs. It is the gateway for browser apps (login, sessions) and for native mobile apps

auth and edge pull their configuration from central. Administrators change configuration only through central, in the admin console or the Central API.

Control plane: central

central stores configuration and serves the Central API (OpenAPI: central.yaml). On an empty database it applies its bootstrap seed once. After that, you change configuration through the console.

The admin console is a static single-page application. It is built into the versola-tools image and served by the proxy at /central/admin/. It has no server of its own: its API calls go to /central/* on the proxy, which routes them through edge to central, so edge sits between the console and central.

Data plane: auth

auth runs the OAuth 2.0 and OIDC flows: /authorize, /par, /token, /introspect, /revoke, /userinfo, logout and discovery. It runs the sign-in conversation (password, OTP, passkey), issues and signs tokens, and stores users. Optionally it runs a second TLS listener for mutual TLS (see sender-constrained tokens).

Traffic plane: edge

edge has three jobs:

  • Browser sessions. It acts as the OAuth client for web apps (/login/{presetId}, /complete). Tokens stay on the server side of the exchange, and the browser holds an EDGE_SESSION cookie (HttpOnly, Secure, SameSite=Strict). Refresh tokens are kept in the edge schema, and edge refreshes expired access tokens.
  • Policy proxy. It proxies /resources/{resourceId}/{path} to your upstream API. For each request it checks revocation, token binding, permissions, audience and the endpoint’s CEL rules, then injects headers or query values built from the token and user. See Edge.
  • Native app gateway. Mobile apps call /native/* endpoints, and edge runs the authorization on their behalf using PAR and a device-held DPoP key. See Native apps.

Databases

Each service owns its data and its migration history, and the services talk to each other only over HTTP APIs. In the standard topology every service has its own Postgres database, so they scale, back up and fail independently. The smallest deployment puts all three in one database, each in its own schema (auth, central, edge). That is what versola bootstrap local and the VPS deployment set up. A service never reads another service’s tables in either topology.

flowchart TB
    admin[["👤 Administrator"]]
    user[["👤 End user"]]

    subgraph versola["⚡ Versola"]
        proxy["versola-proxy<br/><i>nginx</i><br/>Routing and admin console files"]
        edge["edge<br/><i>traffic plane</i><br/>Sessions, policies, native gateway"]
        auth["auth<br/><i>data plane</i><br/>OAuth 2.1 and OIDC server"]
        central["central<br/><i>control plane</i><br/>Configuration and Central API"]
        db[("Postgres<br/>one database<br/>schemas auth, central, edge")]
    end

    upstream{{"🔌 Upstream APIs"}}
    notifications{{"📧 OTP delivery"}}

    admin -->|Console and API| proxy
    user -->|Login and API calls| proxy

    proxy --> edge
    proxy --> auth
    edge -->|Token, PAR, userinfo| auth
    edge -->|Console API calls| central
    edge -.->|Sync| central
    auth -.->|Sync| central
    edge -->|Proxy| upstream
    auth -->|Request OTP| notifications

    central --> db
    auth --> db
    edge --> db

    style admin fill:#92400e,stroke:#92400e,color:#fff
    style user fill:#166534,stroke:#166534,color:#fff
    style proxy fill:#334155,stroke:#e6edf3,color:#fff
    style central fill:#155e75,stroke:#e6edf3,color:#fff
    style edge fill:#155e75,stroke:#e6edf3,color:#fff
    style auth fill:#155e75,stroke:#e6edf3,color:#fff
    style db fill:#0e4a5c,stroke:#e6edf3,color:#fff
    style upstream fill:#334155,stroke:#e6edf3,color:#fff
    style notifications fill:#334155,stroke:#e6edf3,color:#fff
    style versola fill:#eaf3f5,stroke:#155e75,stroke-width:2px,color:#15171c

Ports

PortVariableServes
8080PORTThe service API (auth)
8081DPORT/metrics, /liveness, /readiness
8082APORTAn internal API surface (auth: account settings)
8083MPORTMutual TLS listener of auth, and of edge for /resources, each only when its mutual-tls block is configured

On a CLI-generated server (vps) deployment, central listens on 8090 (diagnostics 8091), edge on 8095 (diagnostics 8096) and versola-proxy on 2821. The local stack also publishes the proxy on 127.0.0.1:2821.

How a request moves

flowchart TB
    admin[["👤 Admin<br/><i>Manages platform</i>"]]
    user[["👤 End User<br/><i>Accesses apps</i>"]]

    versola["⚡ Versola<br/><b>Identity & Authorization Platform</b>"]

    upstream{{"🔌 Upstream<br/>Backend APIs"}}
    notifications{{"📧 Notification Channel<br/>Notifications"}}

    admin -->|Manage| versola
    user -->|Auth| versola
    versola -->|Proxy| upstream
    versola -->|Request Notification| notifications

    style admin fill:#92400e,stroke:#92400e,color:#fff
    style user fill:#166534,stroke:#166534,color:#fff
    style versola fill:#155e75,stroke:#e6edf3,color:#fff,stroke-width:3px
    style upstream fill:#334155,stroke:#e6edf3,color:#fff
    style notifications fill:#334155,stroke:#e6edf3,color:#fff

Browser app

  1. The browser opens GET /login/{presetId} on edge.
  2. edge pushes the authorization request to auth when the client requires PAR, and redirects the browser to auth /authorize with PKCE and state.
  3. auth signs the user in and redirects back to edge /complete with a code.
  4. edge redeems the code at /token, stores the refresh token and sets the EDGE_SESSION cookie.
  5. Each API call carries the cookie. edge validates the token, evaluates the policy and proxies the request.
sequenceDiagram
    autonumber
    participant B as Browser
    participant E as edge
    participant A as auth
    participant U as Upstream

    B->>E: GET /login/:presetId
    E->>A: POST /par (when the client requires PAR)
    E-->>B: 302 → auth /authorize (PKCE, state)

    note over B,A: Challenge flow (password / OTP / passkey)

    A-->>B: 302 → edge /complete?code=…&state=…
    B->>E: GET /complete?code=…
    E->>A: POST /token (code + PKCE verifier)
    A-->>E: access_token + refresh_token
    E->>E: Store refresh token in DB
    E-->>B: 302 → app  +  Set-Cookie: EDGE_SESSION

    note over B,E: Session active — every request carries the cookie

    B->>E: GET /resources/api/v1/data
    E->>E: Validate token, check revocation, evaluate CEL rules
    E->>U: Forward + inject Authorization header (Basic / Bearer)
    U-->>E: 200 OK
    E-->>B: 200 OK (upstream Set-Cookie stripped)

Mobile app through edge

A native client fronted by edge never holds a client credential. edge authenticates to auth as the client with a certificate on the mTLS listener, and the device holds a DPoP key that binds the tokens. The redirect is an App Link, not a custom scheme.

sequenceDiagram
    autonumber
    participant M as Mobile app
    participant E as edge
    participant A as auth
    participant U as Upstream

    M->>E: POST /native/start/{clientId} + DPoP proof (device key)
    E->>A: POST /par as the client (PKCE, state, dpop_jkt)
    E-->>M: request_uri, sealed blob

    note over M,A: System browser opens auth /authorize, user signs in

    A-->>M: Redirect to the App Link with code and state
    M->>E: POST /native/complete/{clientId} + blob + DPoP proof
    E->>A: POST /token (code + PKCE verifier from blob)
    A-->>E: DPoP-bound access_token + refresh_token
    E-->>M: Token response relayed unchanged
    M->>M: Store tokens and device key (Keychain / Keystore)

    note over M,E: Session active — app sends DPoP-bound calls

    M->>E: GET /resources/api/v1/data + Authorization: DPoP + DPoP proof
    E->>E: Check revocation, token binding, permissions and CEL rules
    E->>U: Forward + inject Authorization header
    U-->>E: 200 OK
    E-->>M: 200 OK

    note over M,E: Refresh

    M->>E: POST /native/token/{clientId} (refresh_token) + DPoP proof
    E->>A: POST /token as the client
    A-->>E: new tokens
    E-->>M: new tokens

A native app can also be a public client that talks to auth directly, but only on a tenant with the standard security profile.