← Back to Modules

dbbasic-sessions Specification

Version: 3.0 Status: Specification Author: DBBasic Project Date: October 2025

Links: - PyPI: https://pypi.org/project/dbbasic-sessions/ - GitHub: https://github.com/askrobots/dbbasic-sessions - Specification: http://dbbasic.com/sessions-spec


Philosophy

"Compute, don't store. Verify, don't persist."

Sessions are temporary authentication state. Unix and CGI teach us: don't store temporary state, compute it.

Design Principles

  1. Stateless: No server storage for sessions
  2. Unix-Native: Like HTTP cookies, passwd files - verify credentials, don't store "logged in" state
  3. CGI-Perfect: Each request independent, no shared state
  4. Simple: 15 lines of code
  5. Fast: Pure computation, no I/O

Architecture Decision History

Version 1.0: Individual Files

/tmp/sessions/abc123  (one file per session)
/tmp/sessions/xyz789
...

Problem: Not using dbbasic foundation, filesystem overhead

Version 2.0: TSV Storage

data/sessions.tsv
token   user_id expires
abc123  42  1696886400

Problem: Storing temporary state in persistent storage - Sessions change frequently (stress test on dbbasic-tsv) - Deletes require full file rewrite - Cleanup needed - Not stateless (CGI unfriendly)

Version 3.0: Signed Cookies (FINAL)

token = sign({'user_id': 42}, secret)
# No storage needed

Why this wins: - Unix philosophy: verify credentials each time, don't store "logged in" state - CGI philosophy: stateless, no shared state between processes - Flask/Rails default: signed cookies are the industry standard - Simple: 15 lines vs 20-30 lines with storage - Fast: No I/O, pure computation - Scales: Infinite horizontal scaling


What Sessions Actually Are

Reality check from Django/Rails usage:

Sessions mostly store:

{
    'user_id': 42
}

That's it. Everything else (preferences, cart, profile) is in the database.

The flow:

# Login
token = create_session(user_id=42)
set_cookie('session', token)

# Every request
user_id = get_session(cookie['session'])  # Verify signature
user = User.get(user_id)                   # ACTUAL data from database

Session is just a pointer. Why persist a pointer?


The Unix/CGI Perspective

How Unix Handles Authentication

/etc/passwd and /etc/shadow: - Store permanent user data - Don't store "user is logged in" - Each authentication = verify password hash - No session files

CGI design: - Each script = new process - No shared state - Stateless by nature - Perfect for signed cookies

CGI Session Pattern

#!/bin/bash
# Pure CGI, stateless

USER_ID=$(verify-cookie "$HTTP_COOKIE")
if [ -n "$USER_ID" ]; then
    # Get actual data from permanent storage
    USER_DATA=$(getent passwd "$USER_ID")
    echo "Content-Type: text/html"
    echo ""
    echo "<h1>Welcome $USER_DATA</h1>"
else
    echo "Location: /login"
fi

No session files. Just verify → lookup → render.


Architecture

The dbbasic Approach

┌─────────┐      ┌──────────────┐
│  App    │─────▶│   Verify     │
│ Request │      │  Signature   │
└─────────┘      └──────────────┘
    ↓                    ↓
Cookie token        Pure computation
No storage          No I/O needed
Stateless          CGI-perfect

What's in the cookie:

eyJ1c2VyX2lkIjogNDIsICJleHBpcmVzIjogMTY5Njg4NjQwMH0.a3f8d9e2b1c4...
       ^                                                  ^
    Payload (base64)                              Signature (HMAC)

Payload decoded:

{"user_id": 42, "expires": 1696886400}

Signature: HMAC-SHA256 of payload + secret key


API Specification

Function: create_session(user_id, ttl=2592000)

Purpose: Create signed session token

Parameters: - user_id (str/int): User identifier - ttl (int): Time-to-live in seconds (default: 30 days)

Returns: - token (str): Signed session token

Behavior: 1. Create payload: {'user_id': user_id, 'expires': now + ttl} 2. Base64 encode payload 3. Sign with HMAC-SHA256 using secret key 4. Return payload.signature

Example:

token = create_session(user_id=42)
# token = "eyJ1c2Vy...a3f8d9e2"
# Set as cookie
response.set_cookie('session', token, httponly=True, secure=True)

Function: get_session(token)

Purpose: Verify token and extract user ID

Parameters: - token (str): Session token from cookie

Returns: - user_id (str): User identifier, or None if invalid

Behavior: 1. Split token into payload and signature 2. Verify signature using HMAC-SHA256 3. Decode payload 4. Check if expires > current time 5. Return user_id if valid, None otherwise

Example:

user_id = get_session(request.cookies.get('session'))
if user_id:
    user = User.get(user_id)  # Get actual data from database
    return render('dashboard', user=user)
else:
    return redirect('/login')

Error Handling: - Invalid signature → return None - Expired timestamp → return None - Malformed token → return None - Missing token → return None


Function: destroy_session(token)

Purpose: Logout (client-side operation)

Parameters: - token (str): Session token (not used)

Returns: - None

Behavior: - Nothing server-side - Client deletes cookie

Example:

destroy_session(token)  # No-op server-side
response.delete_cookie('session')
return redirect('/')

Why no server action? Token expires naturally. Can't be reused after client deletes it.


Implementation

Complete Implementation (15 lines)

import hmac, hashlib, json, base64, os, time

SECRET = os.getenv('SECRET_KEY', 'CHANGE-ME-IN-PRODUCTION')

def create_session(user_id, ttl=2592000):
    """Create signed session token"""
    data = {'user_id': str(user_id), 'expires': int(time.time()) + ttl}
    payload = base64.urlsafe_b64encode(json.dumps(data).encode()).decode()
    signature = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return f"{payload}.{signature}"

def get_session(token):
    """Verify and extract user_id from token"""
    try:
        payload, signature = token.split('.')
        if not hmac.compare_digest(signature, hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()):
            return None
        data = json.loads(base64.urlsafe_b64decode(payload))
        return data['user_id'] if data['expires'] > time.time() else None
    except:
        return None

def destroy_session(token):
    """No-op: client deletes cookie"""
    pass

That's it. 15 lines. Zero dependencies beyond stdlib.

Dependencies

Configuration

Required:

export SECRET_KEY="your-secret-key-here"

Generate secret key:

import secrets
print(secrets.token_hex(32))

Important: Change SECRET_KEY in production. Don't use default.


Usage Examples

Web Framework Integration

from dbbasic_sessions import create_session, get_session, destroy_session

@app.route('/login', methods=['POST'])
def login():
    user = authenticate(request.form['email'], request.form['password'])
    if user:
        token = create_session(user.id)
        response = redirect('/dashboard')
        response.set_cookie('session', token,
                          httponly=True,
                          secure=True,
                          samesite='Lax',
                          max_age=2592000)
        return response
    return render('login', error='Invalid credentials')

@app.route('/dashboard')
def dashboard():
    user_id = get_session(request.cookies.get('session'))
    if not user_id:
        return redirect('/login')

    # Get actual user data from database/TSV
    user = User.get(user_id)
    return render('dashboard', user=user)

@app.route('/logout')
def logout():
    response = redirect('/')
    response.delete_cookie('session')
    return response

CGI Script Integration

#!/usr/bin/env python3
import os, sys
from dbbasic_sessions import get_session

# Parse cookie
cookie = os.environ.get('HTTP_COOKIE', '')
token = None
for part in cookie.split(';'):
    if 'session=' in part:
        token = part.split('=', 1)[1].strip()

# Verify session
user_id = get_session(token)

if not user_id:
    print("Location: /login.cgi\n")
    sys.exit()

# Get user data from permanent storage
import pwd
user_info = pwd.getpwuid(int(user_id))

# Render page
print("Content-Type: text/html\n")
print(f"<h1>Welcome, {user_info.pw_gecos}!</h1>")

Middleware Pattern

from dbbasic_sessions import get_session

def require_auth(func):
    """Decorator to require authentication"""
    def wrapper(request, *args, **kwargs):
        user_id = get_session(request.cookies.get('session'))
        if not user_id:
            return redirect('/login')

        # Attach user_id to request
        request.user_id = user_id
        return func(request, *args, **kwargs)
    return wrapper

@require_auth
def profile(request):
    # request.user_id is guaranteed to exist
    user = User.get(request.user_id)
    return render('profile', user=user)

Performance Characteristics

Benchmarks

Operation Time Notes
Create session 0.01ms JSON + base64 + HMAC
Verify session 0.01ms HMAC verify + JSON parse
Destroy session 0ms No-op
Memory usage 0 bytes No server storage

Comparison to Alternatives

Signed cookies (dbbasic):

- HMAC verify: 0.01ms
- JSON parse: <0.001ms
- Total: 0.01ms

Overhead: None
Storage: None
Cleanup: None

Redis:

- TCP connection: 0.1ms
- GET command: 0.05ms
- Total: 0.15ms

Overhead: Redis server (50-100MB)
Storage: ~100 bytes per session
Cleanup: TTL handled by Redis

Database:

- Connection pool: 0.5ms
- SQL query: 1-2ms
- Total: 1.6-2.6ms

Overhead: Database server (100MB+)
Storage: Database row per session
Cleanup: Cron job or lazy deletion

TSV:

- File read: 0.1ms
- Parse TSV: 0.05ms
- Total: 0.15ms

Overhead: File locking
Storage: ~50 bytes per session in file
Cleanup: Cron job to filter expired

Result: Signed cookies are 15x faster than TSV, 160x faster than database

Scaling Characteristics

Concurrent Users Performance Notes
100 <0.01ms Pure computation
1,000 <0.01ms No I/O
10,000 <0.01ms Stateless
100,000 <0.01ms Still instant
1,000,000+ <0.01ms Infinite scale

Scales infinitely: No storage = no bottleneck


Security Considerations

Token Structure

Payload (visible, base64 encoded):

{"user_id": "42", "expires": 1696886400}

Anyone can decode this (base64 is encoding, not encryption)

But they can't modify it without breaking the signature.

Signature Security

HMAC-SHA256: - Cryptographically secure - Secret key never exposed - 256-bit signature - Brute force: 2^256 attempts

Timing-safe comparison:

hmac.compare_digest(signature, expected)

Prevents timing attacks.

Cookie Security Flags

Always set these:

response.set_cookie(
    'session',
    token,
    httponly=True,      # Prevent JavaScript access (XSS protection)
    secure=True,        # HTTPS only
    samesite='Lax',     # CSRF protection
    max_age=2592000     # 30 days
)

Secret Key Management

Generate strong key:

import secrets
SECRET_KEY = secrets.token_hex(32)  # 256 bits

Store securely:

# .env file (never commit)
SECRET_KEY=a3f8d9e2b1c4567890abcdef12345678...

# Environment variable
export SECRET_KEY="a3f8d9e2b1c4567890abcdef12345678..."

Rotate keys:

# Support old and new keys during rotation
SECRET_KEYS = [
    os.getenv('SECRET_KEY'),        # Current
    os.getenv('SECRET_KEY_OLD')     # Previous (grace period)
]

def get_session(token):
    for key in SECRET_KEYS:
        user_id = verify_with_key(token, key)
        if user_id:
            return user_id
    return None

Session Fixation Prevention

Regenerate token after login:

# Don't accept token from query param
# BAD: token = request.args.get('session')

# GOOD: Only from cookie
token = request.cookies.get('session')

# After successful login, generate NEW token
new_token = create_session(user.id)
response.set_cookie('session', new_token)

XSS Prevention

HttpOnly flag prevents JavaScript access:

// This won't work:
document.cookie  // Can't read httponly cookies

Protects against XSS stealing session tokens.

CSRF Protection

SameSite=Lax: - Cookies sent on same-site requests - Not sent on cross-site POST - Prevents CSRF attacks

Additional CSRF token (for forms):

csrf_token = secrets.token_hex(16)
# Include in form + verify on POST

Testing Requirements

Unit Tests

import time
from dbbasic_sessions import create_session, get_session, destroy_session

def test_create_session():
    token = create_session('42')
    assert len(token) > 50  # Base64 + signature
    assert '.' in token     # payload.signature

def test_get_session():
    token = create_session('42')
    user_id = get_session(token)
    assert user_id == '42'

def test_expired_session():
    token = create_session('42', ttl=-1)  # Expired immediately
    time.sleep(0.1)
    user_id = get_session(token)
    assert user_id is None

def test_invalid_signature():
    token = create_session('42')
    # Tamper with token
    payload, sig = token.split('.')
    tampered = f"{payload}.{'0' * 64}"
    user_id = get_session(tampered)
    assert user_id is None

def test_invalid_token():
    user_id = get_session('not-a-token')
    assert user_id is None

def test_concurrent_sessions():
    tokens = [create_session(str(i)) for i in range(100)]
    for i, token in enumerate(tokens):
        assert get_session(token) == str(i)

Performance Tests

import time

def test_performance():
    # Create 10,000 sessions
    start = time.time()
    tokens = [create_session(str(i)) for i in range(10000)]
    create_time = time.time() - start
    print(f"Created 10K sessions in {create_time:.2f}s")

    # Verify 10,000 sessions
    start = time.time()
    for token in tokens:
        get_session(token)
    verify_time = time.time() - start
    print(f"Verified 10K sessions in {verify_time:.2f}s")

    assert create_time < 1.0   # < 1 second to create 10K
    assert verify_time < 1.0   # < 1 second to verify 10K

Security Tests

def test_tampered_payload():
    token = create_session('42')
    payload, sig = token.split('.')

    # Decode, modify, re-encode
    import base64, json
    data = json.loads(base64.urlsafe_b64decode(payload))
    data['user_id'] = '999'  # Try to become different user
    new_payload = base64.urlsafe_b64encode(json.dumps(data).encode()).decode()

    tampered = f"{new_payload}.{sig}"
    user_id = get_session(tampered)
    assert user_id is None  # Should fail verification

def test_replay_after_expiry():
    token = create_session('42', ttl=1)
    user_id = get_session(token)
    assert user_id == '42'

    time.sleep(2)  # Wait for expiry
    user_id = get_session(token)
    assert user_id is None  # Should be expired

Architectural Decisions

Why Signed Cookies Instead of Storage?

Storage approaches tried:

  1. Individual files (/tmp/sessions/token)
  2. Problem: 10,000 files, filesystem overhead, not using dbbasic foundation

  3. TSV storage (data/sessions.tsv)

  4. Problem: Storing temporary state persistently, deletes expensive, cleanup needed

Signed cookies win because: - Unix philosophy: don't store temporary state - CGI philosophy: stateless processes - Industry standard: Flask, Rails default - Simplest: 15 lines vs 20-30 lines - Fastest: No I/O, pure computation - Scales: Infinite horizontal scaling

What About "Logout"?

Question: How do you invalidate signed cookies?

Answer: You don't need to in most cases.

Logout flow: 1. Client deletes cookie 2. Token is gone from browser 3. Can't be used again

But what if token is stolen?

Option 1: Short TTL

create_session(user_id, ttl=3600)  # 1 hour

Stolen token expires quickly.

Option 2: Revocation list (if really needed)

# Add to blacklist on logout
revoked_tokens = set()

def destroy_session(token):
    revoked_tokens.add(token)
    # Or append to file: /data/revoked-tokens

def get_session(token):
    if token in revoked_tokens:
        return None
    return verify_signature(token)['user_id']

Most apps don't need this. Short TTL + HTTPS is sufficient.

Why Not Encrypt the Payload?

Current: Signed (HMAC), not encrypted

eyJ1c2VyX2lkIjogNDJ9.a3f8d9e2...
      ^              ^
  Visible (base64)   Signature

Anyone can decode and see {"user_id": 42}

But they can't modify it without breaking signature.

Why this is fine: - User ID is not secret - Actual user data comes from database - Session just says "I am user 42" - Signature prevents impersonation

If you need encrypted:

from cryptography.fernet import Fernet

cipher = Fernet(SECRET_KEY)
encrypted = cipher.encrypt(json.dumps(data).encode())
token = base64.urlsafe_b64encode(encrypted).decode()

But this adds dependency + complexity. Not needed for most apps.

Sessions vs JWT

This is essentially JWT (JSON Web Tokens), just simpler:

JWT:

eyJhbGc...  .eyJ1c2Vy...  .SflKxwRJ...
   ^            ^              ^
 Header      Payload       Signature

dbbasic-sessions:

eyJ1c2Vy...  .a3f8d9e2...
     ^            ^
  Payload     Signature

Differences: - No header (always HMAC-SHA256) - Simpler implementation - Same security properties

Use JWT if: - Need standard token format - Third-party integrations - Public key verification

Use dbbasic-sessions if: - Simple internal auth - Don't need JWT features - Want minimal code


Common Questions

Q: What if the server restarts?

A: Nothing happens. Tokens still valid (they're signed, not stored).

Unlike TSV/Redis, server state doesn't matter.

Q: What about multiple servers?

A: Works perfectly. All servers share same SECRET_KEY.

# All servers use same secret
export SECRET_KEY="same-secret-on-all-servers"

No session sharing needed. No sticky sessions. No NFS.

Q: How do I "logout all devices"?

A: Rotate the SECRET_KEY:

# Old key no longer validates
SECRET_KEY = "new-secret-key"

# All old tokens become invalid
# Users must re-login

Graceful rotation:

# Accept old + new keys temporarily
SECRET_KEYS = ['new-key', 'old-key']  # 24 hour grace period

Q: Can I store more than user_id?

A: Yes, but keep it small (< 4KB cookie limit):

create_session({
    'user_id': 42,
    'role': 'admin',
    'permissions': ['read', 'write']
})

But consider: Is this session data or application data?

Session = authentication only Application data = database/TSV

Q: What about GDPR/privacy?

A: User ID in cookie is fine. It's not personal data in itself.

Actual personal data (name, email) stays in database, not cookie.

Q: Can token be stolen?

Mitigations: 1. HTTPS only (secure flag) - Can't intercept 2. HttpOnly - Can't steal via XSS 3. Short TTL - Expires quickly 4. SameSite - Can't CSRF

Perfect? No. Good enough? Yes, for 99% of apps.

Q: What if SECRET_KEY is compromised?

A: Rotate immediately:

# Generate new key
NEW_SECRET = secrets.token_hex(32)

# All users must re-login
# Old tokens invalid

Then investigate how key was leaked.


Migration Guide

From TSV Sessions

Old:

from dbbasic_sessions.tsv import create_session, get_session

token = create_session(user_id)  # Writes to TSV
user_id = get_session(token)     # Reads from TSV

New:

from dbbasic_sessions import create_session, get_session

token = create_session(user_id)  # Signs cookie
user_id = get_session(token)     # Verifies signature

Migration: 1. Deploy new version (both work in parallel) 2. New logins get signed tokens 3. Old TSV sessions expire naturally (30 days) 4. Remove TSV code after expiry period

From Flask-Session (Redis/Database)

Old:

from flask_session import Session
app.config['SESSION_TYPE'] = 'redis'
Session(app)

# Automatic session handling
session['user_id'] = user.id

New:

from dbbasic_sessions import create_session, get_session

# Explicit token handling
token = create_session(user.id)
response.set_cookie('session', token)

From Django Sessions

Old:

# Database-backed sessions
request.session['user_id'] = user.id

New:

from dbbasic_sessions import create_session

token = create_session(user.id)
response.set_cookie('session', token)

Deployment

Production Setup

1. Generate secret key:

python3 -c "import secrets; print(secrets.token_hex(32))"

2. Set environment variable:

# .env file
SECRET_KEY=your-generated-key-here

# Or export
export SECRET_KEY="your-generated-key-here"

3. Install package:

pip install dbbasic-sessions

4. Use in app:

from dbbasic_sessions import create_session, get_session

That's it. No database setup, no Redis, no cleanup cron.

Docker Deployment

FROM python:3.11-slim

ENV SECRET_KEY="change-in-production"

RUN pip install dbbasic-sessions

COPY app.py .
CMD ["python", "app.py"]

Environment Variables

SECRET_KEY=required-change-in-production    # Session signing key

That's the only config needed.


Package Structure

dbbasic-sessions/
├── dbbasic_sessions/
│   └── __init__.py          # Complete implementation (15 lines)
├── tests/
│   ├── test_sessions.py
│   ├── test_security.py
│   └── test_performance.py
├── setup.py
├── README.md
├── LICENSE
└── CHANGELOG.md

Success Criteria

This implementation is successful if:

  1. Simple: < 20 lines of code
  2. Fast: < 0.01ms session verification
  3. Stateless: No server storage
  4. CGI-Compatible: Each request independent
  5. Unix-Native: Verify, don't store
  6. Industry Standard: Like Flask/Rails
  7. Scales: Infinite horizontal scaling
  8. Secure: HMAC-SHA256 signed

Comparison to Other Approaches

Approach Lines Storage Cleanup Stateless Multi-Server Speed
Signed Cookies 15 None None 0.01ms
TSV File 20 File Cron NFS 0.15ms
Individual Files 30 10K files Cron NFS 0.05ms
Redis 25 Redis TTL 0.15ms
Database 30 DB Cron 2.0ms

Signed cookies win on all metrics except "server invalidation".

For 99% of apps, client-side deletion is sufficient.


When to Use Something Else

Use Redis/Database if: - Need to force logout remotely - Need to ban user and invalidate all sessions instantly - Storing large session data (> 4KB) - Already running Redis for other reasons - Enterprise compliance requires server-side session control

Otherwise, use signed cookies.


References


Summary

dbbasic-sessions embraces Unix and CGI philosophy:

It works because: - Sessions are just user_id + expires - Signatures prevent tampering - No storage = no cleanup, no scaling issues - Pure computation = fast

Use it when: - Building single or multi-server apps - Want simplest possible sessions - Following Unix/CGI philosophy - Industry-standard approach

Graduate to Redis when: - Need server-side invalidation - Enterprise compliance requires it - Storing > 4KB in session

Until then, use signed cookies. It's the Unix way.


Next Steps: Implement, test, deploy, ship.

No database setup. No Redis. No cleanup cron. No storage.

Just 15 lines of code.