Odin at the gates of Vaulthalla
All posts

Why Vaulthalla Requires a WebSocket Session Before HTTP

A stolen token shouldn’t be a skeleton key. Vaulthalla only honors HTTP requests backed by a live WebSocket session, so access ends when the connection does.

Cooper Larson3 min read

In Vaulthalla, you can’t reach the HTTP layer unless you already have an active WebSocket session. Having authenticated recently isn’t enough, and neither is holding a valid token. You have to be actually connected.

That’s backwards compared to most systems, and it’s entirely intentional.


The Dilemma with Vaulthalla-Web

Vaulthalla is a filesystem-first platform. Performance, consistency, and control over state are foundational requirements, not nice-to-haves.

At the protocol layer, that led to two core requirements:

  • WebSockets act as the primary protocol (low overhead, persistent, stateful)
  • HTTP exists as a secondary interface for:
    • File previews (thumbnails, streaming)
    • Middleware validation (Next.js, edge logic)

That’s simple enough on paper. It gets harder once you try to make both layers obey the same security model.


The Problem: WebSockets Authenticate Before Users Do

A WebSocket authenticates once, during the handshake. Users don’t log in until after the connection is established.

That creates a gap:

You need a valid session before you have a user.

Most systems sidestep this by making HTTP the source of truth and treating WebSockets as an extension. Vaulthalla does the opposite.


The Rule

HTTP requests are only valid if their refresh token maps to an active WebSocket session.

A valid token on its own doesn’t qualify, and neither does a database record. The request has to be backed by a live connection.


How Vaulthalla Handles the Gap

Vaulthalla uses a refresh and access token model, with one key difference:

A refresh token may be issued before authentication, during the WebSocket handshake.

If no token is provided, or the one provided fails validation, a new refresh token is issued immediately and bound to the session.

That means you can hold a valid refresh token with no associated user, before authentication has completed. It sounds uncomfortable at first, but it’s what makes the model work.


Why This Exists

HTTP-based auth is the right choice for the vast majority of applications. Vaulthalla isn’t one of them.

The HTTP layer in Vaulthalla is responsible for:

  • Serving decrypted previews
  • Passing through middleware validation
  • Exposing filesystem-adjacent operations

If a refresh token alone were enough to use those endpoints, a stolen token would work as a skeleton key. Vaulthalla removes that possibility entirely.


What This Changes

Vaulthalla doesn’t validate tokens in isolation. It validates them against live session state.

That has immediate consequences:

  • A stolen refresh token is useless without the session it came from.
  • Sessions can be invalidated instantly: drop the connection and access is revoked.
  • Replay attacks fail because the attacker has no live session.
  • HTTP becomes a projection of a real session rather than an entry point.

If you aren’t connected, you aren’t getting in.


What It Looks Like in Practice

At runtime, Vaulthalla-core runs as a single daemon that coordinates several internal services through a shared hot cache. Authentication state lives in memory, not only in the database.

1. WebSocket Initialization

On connection, the server:

  • Attempts token rehydration
  • Issues a new refresh token if the existing one is invalid or missing
  • Caches the session immediately
1void Manager::tryRehydrate(const std::shared_ptr<Session>& session) {2    try {3        if (!session) return;4 5        Validator::validateRefreshToken(session);6        Issuer::accessToken(session);7        cache(session);8 9        if (!session->user) {10            Issuer::refreshToken(session);11            cache(session);12        }13    } catch (const std::exception& ex) {14        Issuer::refreshToken(session);15        cache(session);16    }17}

2. WebSocket Routing (Implicit Deny)

Unauthenticated sessions can only reach auth routes. Everything else is blocked.

1if (!command.starts_with("auth") && !sessionManager->validate(session, accessToken)) {2    return Response::UNAUTHORIZED(...);3}

3. Session Promotion (Post-Login)

Once the user authenticates:

  • The session is bound to that user
  • The refresh token is upgraded with user context
  • The session becomes authoritative

4. HTTP Validation (The Critical Step)

For each HTTP request, the server:

  • Extracts the refresh token from the cookie
  • Attempts to resolve an active session
  • Validates the request against in-memory session state
1const auto session = sessionManager->validateRawRefreshToken(refresh);

If there’s no session, there’s no access.


Tradeoffs (Because Nothing Is Free)

This model isn’t plug and play, and it has real costs:

  • It requires persistent connections.
  • It adds session lifecycle complexity.
  • It’s harder to reason about than stateless HTTP.
  • It took months to refine into something stable.

You wouldn’t build a CRUD app this way.


Why Vaulthalla Does It Anyway

Vaulthalla isn’t optimizing for simplicity. It’s optimizing for:

  • Control over session state
  • Real-time security guarantees
  • Eliminating token-only attack surfaces
  • Aligning protocol behavior with how the system actually works

A filesystem isn’t stateless, and the auth layer in front of it shouldn’t pretend to be.


Takeaway

Most systems treat WebSockets as optional and HTTP as the source of truth. Vaulthalla reverses that: the WebSocket session is the source of truth, and HTTP exists to serve it.


Vaulthalla isn’t a REST API. It knows whether you’re actually there, and if you aren’t, no token you’re holding will get you access.