Skip to content

Architecture

Surge is designed around one idea: your application code should not care whether auth runs in-process or as a remote SSO server. The switch is a startup choice, not an architectural commitment.

The provider seam

Every auth operation — verify a session, look up an identity, authenticate a password — goes through the AuthProvider trait. There are two implementations:

ProviderBehavior
EmbeddedProviderAll auth logic runs in your process, directly against your database. No network hop.
RemoteProviderCalls forward to a remote surge-server over HTTP, with a built-in cache to reduce round trips.

Your application code works with Arc<dyn AuthProvider> — it never knows which implementation is behind the trait.

Modes at a glance

EmbeddedServed (SSO)
Where auth runsIn your processOn a central surge-server
Where browser login livesMounted on your app at your chosen pathOn surge-server, with redirects to/from your app
Where your frontend calls whoamiYour own appYour own app (same-origin, via your mount) or surge-server (cross-origin with CORS)
DatabaseSharedShared
SessionsWritten directlyWritten via surge-server
Best forSingle service, zero extra infrastructureMultiple services sharing a user base (SSO)

How browser login works in each mode

In both modes, you call provider.browser_router(config) and get back an axum::Router. The provider decides what happens internally:

Embedded (EmbeddedProvider): handlers run in your process. Users visit your-app.com/api/surge/v1/login, complete the flow, and receive a session cookie set on your domain. Everything — credential verification, session minting, whoami, logout — runs locally against your database.

Served (RemoteProvider): the router acts as a reverse proxy, forwarding browser requests to the remote surge-server (which already serves all the same endpoints). Cookie domains are rewritten to your local domain. Your app's consumers see identical routes — they never know whether auth is local or proxied.

The proxy is inside surge-server's trust boundary, not an anonymous client of it: it authenticates with your service token (which needs the browser_proxy grant) and states the end user's address in X-Surge-Client-Ip, so rate limits are keyed on the actual user rather than collapsing your whole user base into your service's one address. surge-server rejects that header unless a valid browser_proxy token accompanies it. See Embedding → Remote mode.

Your app uses the same AuthProvider methods to check sessions and look up identities. The browser routing is mode-agnostic — it's a deployment choice, not a code change.

Switching between modes

rust
// Embedded: Surge runs in your process
let provider = Arc::new(
    EmbeddedProvider::new(EmbeddedConfig {
        database_url:  "...",
        pepper:        "...",
        session_ttl:   Duration::from_secs(72 * 3600),
    }).await?
);

// Served: Surge runs on a central server
let provider = Arc::new(
    RemoteProvider::new(RemoteConfig {
        base_url:       "https://auth.example.com".parse()?,
        service_token:  "...",
        cache_ttl:      Duration::from_secs(60),
        cache_max_entries: 10_000,
        timeout:        Duration::from_secs(5),
    })?
);

Switching modes is a matter of swapping which AuthProvider you construct at startup — the rest of your application code, including how it calls whoami, verifies sessions, or looks up identities, is unchanged.

Mixed mode

One service can embed Surge directly (zero-latency auth for its own routes) while other services connect to the same surge-server via RemoteProvider. All sessions land in the same database — a session minted by an embedded provider is valid when verified by a remote-connected service.

Session stability guarantee

Once a session is minted, it is valid until it expires or is explicitly revoked. Surge version upgrades do not invalidate existing sessions. This holds regardless of provider type (embedded or remote), deployed API version, and whether your services are all on the same version or on different ones.

Security boundaries

  • Passwords: hashed with Argon2id + a site-wide secret pepper (SURGE_PEPPER). The pepper lives in your environment, never in the database.
  • Session tokens: prefixed (aeg_s_…), hashed at rest, shown once on creation.
  • Service tokens: same pattern (aeg_svc_…), with grant-based permissions scoping what each service can do.
  • Login flows: stateful, per-flow CSRF tokens verified in constant time.
  • Rate limiting: windowed counters (per-IP, per-username) applied to authentication endpoints, independent of provider mode.

Data model

All modes share a single Postgres schema (surge) with these core tables:

TablePurpose
identityUser accounts (username, display name, avatar, state)
credential_passwordPassword hashes (Argon2id, versioned for algorithm upgrades)
sessionActive sessions (hashed token, identity, expiry, metadata)
login_flowIn-progress login flows (state, CSRF token, attempt tracking)
serviceService tokens and their grants
audit_logStructured security event trail
rate_limit_windowWindowed rate limit counters

Migrations run at startup — automatically when using EmbeddedProvider, or when surge-server starts in served mode. You own the database; Surge manages the schema.