← 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.