DEV Community

XaviorCross6845
XaviorCross6845

Posted on

Postgres Access Ledger: 3 Retention Rules for Inventory and Remote Sign-Out

Short answer: build the account security center on an authoritative server-side session ledger, make remote sign-out an atomic revocation rather than a UI deletion, and keep recovery capable of invalidating every session without depending on the signup captcha.

For a logistics portal, the expensive part isn't rendering a list of browsers. It is continuously writing activity timestamps, retaining high-cardinality request evidence, and sending recovery messages when bot registrations get through. Start with those terms. A useful rough model is session writes + audit bytes + challenge checks + recovery deliveries; the first two usually move with traffic and retention, while the latter two move with abuse and support load. The architecture should make each term visible before anyone debates vendors.

What does the session bill actually contain?

Suppose the portal receives 500 authenticated requests per second. Updating last_seen_at on every request would produce 43.2 million row updates per day. That number is arithmetic, not a benchmark, and your mileage may vary with traffic shape. Still, it exposes the bad coupling: page activity has become database write activity.

Throttle the activity heartbeat instead. A session that has already recorded activity within the last 15 minutes can skip the write. The inventory remains useful for a dispatcher asking, "Is that warehouse tablet still active?", but opening a tracking page no longer creates another durable write. The exact interval is a product decision: five minutes gives a fresher label; an hour spends fewer writes but makes "active" a much weaker promise. Don't label either value "online." Say when the service last observed an authenticated request.

The second term is retention. A current-session row is small, but raw IP addresses, complete user-agent strings, request traces, and immutable audit events multiply across sessions and backups. Keep operational state and security evidence in separate tables with separate deletion schedules. The current table answers what can be revoked now. The audit table answers who requested a revocation and what changed. A sample policy might keep active and unexpired session state, retain coarse security events for a defined investigation window, and delete raw network metadata sooner. Those are example policy choices, not universal compliance periods; legal, fraud, and support owners need to approve the actual windows.

There is a real cost to deleting evidence. After the investigation window closes, the team may be unable to reconstruct the precise source address behind an old sign-in. That is deliberate data minimization, but it narrows forensic reach when an account owner reports an incident late. If long-horizon attribution is mandatory, retain protected audit evidence longer and accept the storage, access-control, and privacy burden. The security center shouldn't quietly make that call for the organization.

How should an account security center handle session inventory and remote sign-out?

Treat each authenticated device as a revocable server-side record. The browser gets an opaque, random session token in a Secure, HttpOnly cookie; the database stores a one-way digest rather than the bearer value. The row needs a stable session ID, user ID, creation time, last observed time, absolute expiry, and optional revocation time. Device labels are display hints derived from untrusted request metadata, never proof that a device is genuine.

Inventory reads must be scoped by the authenticated user at the query boundary. Returning every row and filtering in application code is too late. The response should expose only what the owner needs: a session identifier safe to send back for revocation, a recognizable label, approximate activity, creation time, and whether this is the current session. Avoid returning token digests, full IP history, or internal fraud signals.

Remote sign-out is a state transition. In one transaction, lock the target row, verify that it belongs to the caller, set revoked_at if it is still live, and append an audit event. Repeating the same request should return the same effective result, because double-clicks and retries are ordinary network behavior. Every authenticated request then checks that the session is unexpired and not revoked. If the application uses a cache to avoid a database lookup on every request, revocation needs a cache invalidation path and a stated maximum propagation delay. Without that contract, "sign out" means something different in each service.

Use a distinct command for "sign out all other sessions." It should revoke every live row for the account except the caller's session in a single database transaction. A password-reset completion may need the stricter variant that revokes every session, including the recovery browser, then issues a fresh session only after the reset succeeds. That choice is why account recovery is the primary axis here: a polished device list doesn't help if a stolen session survives credential recovery.

Put recovery outside the captcha trust boundary

The signup captcha protects one operation: creating an account. It can reduce automated registrations, but it doesn't establish that a later recovery request belongs to the account owner. Recovery needs its own rate limits, one-time and expiring secrets, uniform outward responses for existing and nonexistent accounts, and a delivery path that doesn't disclose account state. OWASP also recommends invalidating recovery tokens after use and avoiding automatic account changes before a valid token is presented.

A solved signup captcha is not evidence that the person requesting recovery controls the account.

For a logistics system, recovery has awkward edge cases. A driver may lose a phone during a route. A shared warehouse workstation may still hold a valid cookie. An email address may be controlled by a central operations team rather than the person holding the device. The recovery design must answer who can recover which account, which sessions are revoked, and what support can do when the normal channel is unavailable. CAPTCHA placement is secondary.

Keep the branches explicit:

  1. A normal remote sign-out revokes one selected session and leaves the current session alive.
  2. A suspected compromise action revokes all sessions and requires fresh authentication.
  3. A completed password recovery consumes the recovery secret atomically, revokes existing sessions according to policy, and records a security event.
  4. A failed or expired recovery attempt changes no session state and reveals no account existence through its public response.

The catch is that immediate global revocation is not suitable for every operational account. A warehouse kiosk in the middle of a handoff may lose access at a costly moment, and shared accounts make the inventory misleading because a "device" cannot identify the person using it. Stick with individually assigned accounts plus managed-device controls when human attribution matters. For service accounts or unattended scanners, use workload credentials and a separate credential-rotation workflow; don't force them into a consumer-style device screen.

Make revocation atomic in Python

The following repository-shaped Python keeps policy out of the HTTP handler. SQL parameter syntax and transaction APIs vary by driver, so adapt the calls to the library already used by the service. The important properties are ownership checking, a row lock, an idempotent update, and an audit write in the same transaction.

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Protocol


class Database(Protocol):
    def transaction(self): ...


@dataclass(frozen=True)
class RevocationResult:
    session_id: str
    revoked_at: datetime
    already_revoked: bool


def revoke_session(
    db: Database,
    *,
    owner_user_id: str,
    target_session_id: str,
    actor_session_id: str,
) -> RevocationResult:
    now = datetime.now(timezone.utc)

    with db.transaction() as tx:
        session = tx.fetch_one(
            """
            SELECT id, revoked_at
              FROM auth_sessions
             WHERE id = %(session_id)s
               AND user_id = %(user_id)s
             FOR UPDATE
            """,
            {"session_id": target_session_id, "user_id": owner_user_id},
        )
        if session is None:
            raise LookupError("session not found")

        already_revoked = session["revoked_at"] is not None
        revoked_at = session["revoked_at"] or now

        if not already_revoked:
            tx.execute(
                """
                UPDATE auth_sessions
                   SET revoked_at = %(revoked_at)s
                 WHERE id = %(session_id)s
                """,
                {"revoked_at": revoked_at, "session_id": target_session_id},
            )
            tx.execute(
                """
                INSERT INTO security_events
                    (user_id, event_type, target_session_id,
                     actor_session_id, occurred_at)
                VALUES
                    (%(user_id)s, 'session_revoked', %(target_session_id)s,
                     %(actor_session_id)s, %(occurred_at)s)
                """,
                {
                    "user_id": owner_user_id,
                    "target_session_id": target_session_id,
                    "actor_session_id": actor_session_id,
                    "occurred_at": now,
                },
            )

    return RevocationResult(target_session_id, revoked_at, already_revoked)
Enter fullscreen mode Exit fullscreen mode

The handler should map an inaccessible target to a generic not-found response, rather than revealing that another user's session ID exists. It also needs recent-authentication or step-up policy for destructive actions, CSRF protection when cookie authentication is used, and rate limits. Those controls sit around the transaction; they don't replace it.

Short code, hard contract.

Test the states people actually reach

Happy-path tests are cheap. The release gate should also cover two simultaneous revocation requests, a target already expired, a target owned by another account, a stale inventory page, and a request that arrives immediately after revocation. Verify database state and audit state, not merely the HTTP status. A transaction rollback must leave both unchanged.

Test recovery as a state machine. A recovery secret should have one successful consumer. Two concurrent submissions must not both create fresh authenticated sessions. After a successful high-risk recovery, replay the old cookie against every service that accepts it and confirm rejection. I'm not sure a security center can make a defensible "signed out" claim unless this cross-service check is part of deployment testing; the answer depends on where authentication is enforced, and a service inventory resolves that uncertainty.

Observability should count transitions without copying secrets into logs. Useful signals include revocation commands, idempotent repeats, ownership-check failures, session-validation failures after revocation, recovery requests, consumed recovery secrets, and delivery outcomes grouped by coarse reason. Alert on changes from an established baseline, not a universal threshold. Email and SMS delivery are external systems with their own delays and filtering, so the recovery UI should state that a request was accepted without promising arrival or exposing whether the account exists.

Roll out in stages. First write the ledger while the old authentication path remains authoritative. Then compare validation decisions, expose inventory to internal accounts, enable single-session revocation, and finally connect compromise and recovery flows to global revocation. Keep a rollback plan for the enforcement switch. Do not roll back the audit trail.

References

Further reading

Top comments (0)