Version: 1.0 Status: Specification Author: DBBasic Project Date: October 2025
Links: - PyPI: https://pypi.org/project/dbbasic-accounts/ - GitHub: https://github.com/askrobots/dbbasic-accounts - Specification: http://dbbasic.com/accounts-spec
"The web forked from Unix. Let's merge it back."
The critical insight: Unix already solved user management, authentication, and permissions. The web abandoned it and reinvented everything poorly. What if we just... used Unix?
/etc/passwd, /etc/shadow, /etc/group - don't reinvent# /etc/passwd
john:x:1000:100:John Doe:/home/john:/bin/bash
# /etc/shadow (passwords)
john:$6$salt$hash:19000:0:99999:7:::
# /etc/group
editors:x:101:john,jane
admins:x:102:john
Every Unix app used this:
- Email (sendmail) → mail to john@hostname
- FTP → login with passwd credentials
- SSH → same credentials
- CGI scripts → same credentials
- Forums, wikis → same credentials
One user database for everything.
The problem: Shared hosting
Server has 1000 websites
Each site needs different users
Can't use /etc/passwd (would conflict)
The "solution": Every app invents its own user system
WordPress → wp_users table
phpBB → phpbb_users table
Drupal → drupal_users table
Custom apps → users table (everyone invents their own)
The loss:
- No standard user database
- Can't share users across apps
- Every app reinvents authentication
- No Unix tools work (can't use passwd, groups, etc.)
Everyone builds their own: - Django: User model, auth framework - Rails: Devise, Authlogic, etc. - Laravel: Built-in auth - Express: Passport.js (100+ strategies)
Each slightly different, none Unix-compatible.
Docker = isolated Unix system per app
Each container has its own:
- /etc/passwd
- /etc/shadow
- /etc/group
No conflicts anymore! Each web app can have its own /etc/passwd without conflicting.
We can return to Unix.
Your web app IS a Unix system:
myapp/ (Docker container or directory)
├── etc/
│ ├── passwd.tsv # Users (like /etc/passwd)
│ ├── shadow.tsv # Passwords (like /etc/shadow)
│ └── group.tsv # Groups/roles (like /etc/group)
├── home/ # User directories
│ ├── john/ # User john's files
│ │ ├── profile.jpg
│ │ └── uploads/
│ └── jane/
├── var/
│ ├── mail/ # Email spool (user inboxes)
│ └── log/ # Logs
└── app.py # Web app
Now Unix tools work:
# List users
cat etc/passwd.tsv
# Change password
dbpasswd passwd john
# Check groups
dbpasswd groups john
# Send email to user
echo "Hello" | mail john
# View user's files
ls home/john/
# Check user's mailbox
cat var/mail/john
Your web app, Unix commands, and Docker all use the same user database.
FROM python:3.11
COPY app.py /app/
# Users stored in MySQL/PostgreSQL/custom database
# No Unix integration
Problems: - Web app users ≠ Unix users - Can't use Unix tools - Everything custom
FROM python:3.11
# Web app creates its own user database
COPY app.py /app/
COPY etc/ /app/etc/
# Now Unix tools work with web users
RUN dbpasswd useradd john --fullname "John Doe <john@example.com>"
RUN dbpasswd groupadd editors
RUN dbpasswd usermod john --add-group editors
CMD ["python", "app.py"]
Each container = isolated Unix system with its own users.
No conflicts. No shared /etc/passwd. Each app independent.
/etc/passwd ✅ (we have this)/etc/shadow ✅ (we have this)/etc/group ✅ (we have this)sendmail, local mail ❌ (missing)/home/user ❌ (missing)/var/mail/user ❌ (missing)For a complete "Unix web system":
# User creates account (web registration)
accounts.register('john@example.com', 'secret', name='John Doe')
# This creates:
# 1. etc/passwd.tsv entry
# 2. etc/shadow.tsv entry
# 3. home/john/ directory (user's files)
# 4. var/mail/john (user's inbox)
# Now user can:
# - Login to web app
# - Upload files to home/john/uploads/
# - Receive mail at var/mail/john
# - Use Unix tools on their data
Everything integrated.
Unix layer (foundation):
from dbbasic_accounts import PasswdDB
passwd = PasswdDB('./etc')
user = passwd.useradd('john', password='secret', fullname='John Doe <john@example.com>')
passwd.usermod('john', add_groups=['editors'])
authenticated = passwd.authenticate('john', 'secret')
Web layer (convenience):
from dbbasic_accounts import Accounts
accounts = Accounts('./etc', domain='example.com')
user = accounts.register('john@example.com', 'secret', name='John Doe')
accounts.add_role('john@example.com', 'editor')
authenticated = accounts.login('john@example.com', 'secret')
Both use the same underlying files.
Purpose: Register new user (web-friendly)
Parameters:
- email (str): Email address (becomes username@domain)
- password (str): Plain text password (will be hashed)
- name (str): Display name
Returns:
- User object
Behavior:
1. Extract username from email (john@example.com → john)
2. Create entry in etc/passwd.tsv
3. Hash password and store in etc/shadow.tsv
4. Add to 'users' group in etc/group.tsv
5. Create home/{username}/ directory
6. Create var/mail/{username} mailbox
7. Return User object
Example:
user = accounts.register('john@example.com', 'secret123', name='John Doe')
# Creates:
# - etc/passwd.tsv: john, 1000, 100, "John Doe <john@example.com>", /home/john, /bin/bash
# - etc/shadow.tsv: john, $argon2id$..., 2025-10-09
# - etc/group.tsv: users group with john as member
# - home/john/ directory
# - var/mail/john file
Purpose: Authenticate user (web-friendly)
Parameters:
- email (str): Email address
- password (str): Plain text password
Returns:
- User object if valid, None otherwise
Example:
user = accounts.login('john@example.com', 'secret123')
if user:
session['user_id'] = user.uid
session['username'] = user.username
Purpose: Get user by ID, email, or username
Example:
user = accounts.get_user(user_id=1000)
user = accounts.get_user(email='john@example.com')
user = accounts.get_user(username='john')
Standard Unix useradd.
Standard Unix passwd.
Standard Unix usermod.
Standard Unix groups command.
etc/
├── passwd.tsv # User database (like /etc/passwd)
├── shadow.tsv # Password hashes (like /etc/shadow, chmod 600)
└── group.tsv # Groups/roles (like /etc/group)
passwd.tsv:
username uid gid fullname homedir shell created
john 1000 100 John Doe <john@example.com> /home/john /bin/bash 2025-10-09T10:30:00
jane 1001 100 Jane Smith <jane@example.com> /home/jane /bin/bash 2025-10-09T11:00:00
shadow.tsv:
username password_hash last_changed min_age max_age warn_age inactive
john $argon2id$v=19$m=65536,t=3,p=4$... 2025-10-09T10:30:00 0 90 7 14
jane $argon2id$v=19$m=65536,t=3,p=4$... 2025-10-09T11:00:00 0 90 7 14
group.tsv:
groupname gid members
users 100
editors 101 john,jane
admins 102 john
home/
├── john/
│ ├── profile.jpg
│ ├── uploads/
│ │ ├── photo1.jpg
│ │ └── document.pdf
│ └── settings.json
└── jane/
└── avatar.png
Each user has their own directory (like Unix /home)
var/mail/
├── john # Mailbox for john (mbox format)
└── jane # Mailbox for jane
Internal messaging, notifications, etc.
from dbbasic_accounts import Accounts
from dbbasic_sessions import create_session, get_session
accounts = Accounts('./etc')
@app.route('/login', methods=['POST'])
def login():
user = accounts.login(
request.form['email'],
request.form['password']
)
if user:
# Create session (signed cookie)
token = create_session(user.uid)
response = redirect('/dashboard')
response.set_cookie('session', token, httponly=True)
return response
return 'Invalid credentials', 401
@app.route('/dashboard')
def dashboard():
user_id = get_session(request.cookies.get('session'))
if not user_id:
return redirect('/login')
user = accounts.get_user(user_id=user_id)
return render('dashboard.html', user=user)
from dbbasic_accounts import Accounts
from dbbasic_email import send_email
accounts = Accounts('./etc')
# Send to user
user = accounts.get_user(email='john@example.com')
send_email(
to=user.email, # Extracted from fullname
subject='Welcome!',
body='Thanks for registering'
)
# Or use Unix mail (internal messaging)
accounts.mail('john', 'You have a new message')
# → Appends to var/mail/john
from dbbasic_accounts import Accounts
from dbbasic_upload import save_upload
accounts = Accounts('./etc')
user = accounts.get_user(user_id=current_user_id)
# Save upload to user's home directory
save_upload(
file=request.files['photo'],
path=f"{user.homedir}/uploads/profile.jpg"
)
# → Saves to home/john/uploads/profile.jpg
Web App
├── MySQL database (users table)
├── Redis (sessions)
├── S3 (file uploads)
├── SendGrid (email)
└── Custom auth code
Everything separate, nothing integrated.
Web App (Unix System)
├── etc/passwd.tsv # Users
├── home/{user}/ # User files
├── var/mail/{user} # User mail
├── data/*.tsv # Application data
└── app.py # Web interface to Unix system
Everything integrated, Unix tools work.
Scenario: User Registration
Traditional:
# Insert into database
db.execute("INSERT INTO users ...")
# Send welcome email (external service)
sendgrid.send(email, "Welcome")
# Create S3 bucket for uploads
s3.create_bucket(f"user-{user_id}")
Unix/dbbasic:
# Register user (creates user, home dir, mailbox)
user = accounts.register('john@example.com', 'secret')
# Welcome email (local delivery)
mail('john', 'Welcome to the site!')
# → Appends to var/mail/john
# User uploads
save_file(f'home/{user.username}/uploads/file.jpg')
# → Saves to local filesystem
Everything unified through Unix.
1. Unified user database
# One database for all apps
getent passwd john
2. Local email
# Send mail to user
echo "Hello" | mail john
# Read mail
mail
3. User home directories
# Each user has space
ls /home/john
4. Standard tools
# Change password
passwd
# Check groups
groups
# Who's logged in
w, who
5. Permissions
# File ownership
chown john:editors file.txt
chmod 640 file.txt
For dbbasic to be "Unix for web apps":
dbbasic-accounts (passwd.tsv, shadow.tsv, group.tsv)dbbasic-mail (var/mail/* mailboxes)dbpasswd, dbmail, etc.# dbbasic_accounts/__init__.py
from .passwd import PasswdDB, User
from .accounts import Accounts
__all__ = ['PasswdDB', 'User', 'Accounts']
class PasswdDB:
"""
Unix-style user management.
Mirrors /etc/passwd, /etc/shadow, /etc/group
"""
def __init__(self, etc_dir='./etc'):
self.etc_dir = Path(etc_dir)
self.passwd_path = self.etc_dir / 'passwd.tsv'
self.shadow_path = self.etc_dir / 'shadow.tsv'
self.group_path = self.etc_dir / 'group.tsv'
self._init_files()
self.ph = PasswordHasher() # Argon2id
def useradd(self, username, password, fullname='', homedir=None, shell='/bin/bash', groups=None):
"""Add user (like Unix useradd)"""
uid = self._next_uid()
gid = 100 # users group
homedir = homedir or f'/home/{username}'
# Add to passwd.tsv
append(self.passwd_path, [username, uid, gid, fullname, homedir, shell, now()])
# Hash password and add to shadow.tsv
password_hash = self.ph.hash(password)
append(self.shadow_path, [username, password_hash, now(), 0, 90, 7, 14])
# Create home directory
Path(f'.{homedir}').mkdir(parents=True, exist_ok=True)
# Add to groups
if groups:
for group in groups:
self.usermod(username, add_groups=[group])
return User(username, uid, gid, fullname, homedir, shell, now())
def authenticate(self, username, password):
"""Authenticate user"""
# Get hash from shadow.tsv
shadow = get(self.shadow_path, username=username)
if not shadow:
return None
# Verify password
try:
self.ph.verify(shadow.password_hash, password)
return self.getuser(username)
except VerifyMismatchError:
return None
def passwd(self, username, new_password):
"""Change password (like Unix passwd)"""
password_hash = self.ph.hash(new_password)
update(self.shadow_path,
where={'username': username},
values={'password_hash': password_hash, 'last_changed': now()}
)
def groups(self, username):
"""Get user's groups (like Unix groups command)"""
user = self.getuser(username)
groups = []
for group in get_all(self.group_path):
# Primary group
if group.gid == user.gid:
groups.append(group.groupname)
# Member of group
elif username in group.members.split(','):
groups.append(group.groupname)
return groups
class Accounts:
"""
Web-friendly wrapper around Unix passwd.
Provides email-based authentication and role management.
"""
def __init__(self, etc_dir='./etc', domain='example.com'):
self.passwd = PasswdDB(etc_dir)
self.domain = domain
def register(self, email, password, name=''):
"""Register user (web-friendly)"""
username = email.split('@')[0]
fullname = f"{name} <{email}>" if name else email
user = self.passwd.useradd(username, password, fullname=fullname)
# Send welcome email to user's mailbox
self.mail(username, 'Welcome!', 'Thanks for registering.')
return user
def login(self, email, password):
"""Authenticate by email"""
username = email.split('@')[0]
return self.passwd.authenticate(username, password)
def get_user(self, user_id=None, email=None, username=None):
"""Get user by ID, email, or username"""
if email:
username = email.split('@')[0]
if username:
return self.passwd.getuser(username)
if user_id:
# Find by UID
users = get_all(self.passwd.passwd_path)
for u in users:
if u.uid == user_id:
return u
return None
def has_role(self, email, role):
"""Check if user has role (groups alias)"""
username = email.split('@')[0]
return role in self.passwd.groups(username)
def add_role(self, email, role):
"""Add role to user"""
username = email.split('@')[0]
self.passwd.usermod(username, add_groups=[role])
def mail(self, username, subject, body):
"""Send mail to user's mailbox (Unix mail)"""
mailbox = Path(f'./var/mail/{username}')
mailbox.parent.mkdir(parents=True, exist_ok=True)
# Append to mailbox (mbox format)
with mailbox.open('a') as f:
f.write(f"From system {datetime.now()}\n")
f.write(f"Subject: {subject}\n")
f.write(f"\n{body}\n\n")
@property
def email(self, username):
"""Get user's email from fullname field"""
user = self.passwd.getuser(username)
if not user:
return None
# Extract from "Name <email>" format
if '<' in user.fullname and '>' in user.fullname:
return user.fullname.split('<')[1].split('>')[0]
# Fallback to username@domain
return f"{username}@{self.domain}"
myapp/
├── app.py # Web application
├── etc/ # User database (Unix /etc)
│ ├── passwd.tsv
│ ├── shadow.tsv
│ └── group.tsv
├── home/ # User home directories (Unix /home)
│ ├── john/
│ │ ├── uploads/
│ │ └── profile.jpg
│ └── jane/
├── var/ # Variable data (Unix /var)
│ ├── mail/ # User mailboxes
│ │ ├── john
│ │ └── jane
│ └── log/ # Logs (from dbbasic-logs)
├── data/ # Application data (dbbasic-tsv)
│ ├── posts.tsv
│ └── comments.tsv
└── static/ # Web static files
This IS a Unix system - just for your web app.
from dbbasic_web import app, route, render
from dbbasic_accounts import Accounts
from dbbasic_tsv import get, insert
from dbbasic_sessions import create_session, get_session
accounts = Accounts('./etc', domain='myblog.com')
@route('/register', methods=['POST'])
def register():
user = accounts.register(
email=request.form['email'],
password=request.form['password'],
name=request.form['name']
)
# User now has:
# - Account in etc/passwd.tsv
# - Password in etc/shadow.tsv
# - Home directory in home/john/
# - Mailbox in var/mail/john
# Send welcome message to their mailbox
accounts.mail(user.username,
'Welcome!',
'Thanks for registering. Your home directory is ready.'
)
return redirect('/login')
@route('/upload', methods=['POST'])
def upload():
user_id = get_session(request.cookies.get('session'))
user = accounts.get_user(user_id=user_id)
# Save to user's home directory
file = request.files['photo']
file.save(f'{user.homedir}/uploads/{file.filename}')
# Notify user via internal mail
accounts.mail(user.username,
'File uploaded',
f'Uploaded {file.filename} to your home directory'
)
return 'Uploaded!'
@route('/admin')
def admin():
user_id = get_session(request.cookies.get('session'))
user = accounts.get_user(user_id=user_id)
# Check Unix group membership
if not accounts.has_role(user.email, 'admins'):
return 'Forbidden', 403
return render('admin.html')
Unix commands work:
# List users
cat etc/passwd.tsv
# Change user's password
dbpasswd passwd john
# Check who's an admin
grep admins etc/group.tsv
# View user's files
ls home/john/uploads/
# Read user's mail
cat var/mail/john
# Make someone an admin
dbpasswd usermod john --add-group admins
1. Standard tools work
# No custom admin panel needed
cat etc/passwd.tsv | wc -l # Count users
grep admin etc/group.tsv # Find admins
ls home/*/uploads/ | wc -l # Count uploaded files
2. Integration for free - User signs up → gets mailbox automatically - User signs up → gets home directory automatically - User signs up → can receive mail automatically
3. Debuggability
# Is user in admin group?
grep john etc/group.tsv
# What's their home directory?
grep john etc/passwd.tsv
# What mail do they have?
cat var/mail/john
4. Docker-native - Each container = isolated Unix system - No user conflicts - Standard Unix tools work inside container
5. Educational - Web developers learn Unix - Unix admins understand web apps - Same concepts, same tools
Old (Django):
from django.contrib.auth.models import User
user = User.objects.create_user('john', 'john@example.com', 'password')
user.groups.add(editors_group)
New (dbbasic):
from dbbasic_accounts import Accounts
accounts = Accounts('./etc')
user = accounts.register('john@example.com', 'password', name='John')
accounts.add_role('john@example.com', 'editors')
Data migration:
# Export from Django
for user in User.objects.all():
accounts.passwd.useradd(
username=user.username,
password='reset-required', # Force password reset
fullname=f"{user.first_name} {user.last_name} <{user.email}>"
)
# Migrate groups → roles
for group in user.groups.all():
accounts.add_role(user.email, group.name)
Old (WordPress):
SELECT * FROM wp_users WHERE user_login = 'john';
SELECT * FROM wp_usermeta WHERE user_id = 1 AND meta_key = 'wp_capabilities';
New (dbbasic):
user = accounts.login('john@example.com', 'password')
if accounts.has_role(user.email, 'administrator'):
# Is admin
Data migration:
# Import WordPress users
import MySQLdb
db = MySQLdb.connect('localhost', 'root', 'password', 'wordpress')
cursor = db.cursor()
cursor.execute("SELECT user_login, user_email, display_name FROM wp_users")
for username, email, display_name in cursor.fetchall():
accounts.passwd.useradd(
username=username,
password='reset-required', # Force password reset
fullname=f"{display_name} <{email}>"
)
# Map WordPress roles to groups
# administrator → admins
# editor → editors
# etc.
Argon2id (winner of Password Hashing Competition):
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # 64 MB
parallelism=4 # 4 threads
)
hash = ph.hash('password')
# $argon2id$v=19$m=65536,t=3,p=4$salt$hash
Why Argon2id: - Current best practice (2015+) - Resists GPU/ASIC attacks - Memory-hard (can't parallelize on GPUs) - Better than bcrypt, scrypt, PBKDF2
# Automatically set on creation
os.chmod('etc/shadow.tsv', 0o600)
# Owner read/write only (like /etc/shadow)
Only the app can read passwords.
def reset_password(email, token):
"""Reset password with token"""
# Verify token (time-limited, signed)
username = verify_reset_token(token)
if not username:
return False
# Generate temporary password
temp_password = secrets.token_hex(16)
# Update password
accounts.passwd.passwd(username, temp_password)
# Send to email
send_email(email, 'Password Reset', f'Temporary password: {temp_password}')
return True
#!/usr/bin/env python3
# dbbaswd - Unix-style user management for dbbasic apps
import sys
from dbbasic_accounts import PasswdDB
passwd = PasswdDB('./etc')
if sys.argv[1] == 'useradd':
username = sys.argv[2]
password = input('Password: ')
user = passwd.useradd(username, password)
print(f"Created user {username} (UID {user.uid})")
elif sys.argv[1] == 'passwd':
username = sys.argv[2]
new_password = input('New password: ')
passwd.passwd(username, new_password)
print(f"Password changed for {username}")
elif sys.argv[1] == 'groups':
username = sys.argv[2]
groups = passwd.groups(username)
print(' '.join(groups))
elif sys.argv[1] == 'list':
for user in passwd.list_users():
print(f"{user.username}:{user.uid}:{user.gid}:{user.fullname}")
Now Unix commands work:
dbpasswd useradd john
dbpasswd passwd john
dbpasswd groups john
dbpasswd list
Your web app running in Docker:
FROM python:3.11
# This is a Unix system
COPY etc/ /app/etc/
COPY home/ /app/home/
COPY var/ /app/var/
# Unix tools work
RUN cat /app/etc/passwd.tsv
RUN ls /app/home/*/
# Web app uses same user database
COPY app.py /app/
CMD ["python", "/app/app.py"]
Inside the container:
# SSH into running container
docker exec -it myapp bash
# Unix tools work with web users
cat etc/passwd.tsv
mail john < message.txt
ls home/john/uploads/
grep admin etc/group.tsv
Your web app IS a Unix system.
Later, you could even:
Docker Compose:
- blog app (etc/passwd.tsv)
- forum app (etc/passwd.tsv)
- wiki app (etc/passwd.tsv)
Shared volume: /shared/etc/
All apps use same user database!
Just like Unix in the 1990s - one user database, all apps use it.
But that's phase 2. For now, each app = isolated Unix system.
Django:
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
user = User.objects.create_user('john', 'john@example.com', 'password')
authenticated = authenticate(username='john', password='password')
Pros: - Familiar to Django developers - Integrated with Django
Cons: - Requires Django - Database required - Not Unix-compatible - Custom implementation
dbbasic:
from dbbasic_accounts import Accounts
accounts = Accounts('./etc')
user = accounts.register('john@example.com', 'password', name='John')
authenticated = accounts.login('john@example.com', 'password')
Pros: - Unix-compatible - TSV files (no database needed) - Standard tools work - Simple implementation
Cons: - New to web developers - Requires learning Unix concepts
Django approach: Familiar to web developers, custom implementation
dbbasic approach: Unix-compatible, standard implementation
dbbasic bet: Teaching Unix concepts is better than inventing new ones.
dbbasic-accounts/
├── dbbasic_accounts/
│ ├── __init__.py # Exports
│ ├── passwd.py # PasswdDB (Unix layer)
│ ├── accounts.py # Accounts (web layer)
│ └── cli.py # CLI commands (dbpasswd)
├── tests/
│ ├── test_passwd.py
│ ├── test_accounts.py
│ ├── test_integration.py
│ └── test_security.py
├── bin/
│ └── dbpasswd # CLI tool
├── setup.py
├── README.md
├── LICENSE
└── CHANGELOG.md
This implementation is successful if:
The web didn't need to fork from Unix.
We abandoned Unix because of shared hosting constraints. Multiple websites on one server couldn't share /etc/passwd.
Docker solves this. Each container has its own /etc/.
So we can return to Unix.
dbbasic-accounts is the first step in making web apps Unix systems again.
Goal: Make web apps Unix systems again.
Philosophy: Don't reinvent. Return to what worked.
dbbasic-accounts brings Unix user management to web apps:
What it is: - TSV-based /etc/passwd, /etc/shadow, /etc/group - Dual API (Unix + web-friendly) - Home directories, mailboxes, groups - Standard Unix tools work
Why it matters: - Web apps can be Unix systems again - Docker enables isolated user databases - No need to reinvent authentication - 50 years of battle-tested design
Use it when: - Building web apps with Docker - Want Unix tool compatibility - Value standard over custom - Building the future by honoring the past
This is how we merge the fork.
The web returns to Unix.
Next Steps: Implement, test, deploy, ship.
No custom user tables. No reinvented auth. No fork.
Just Unix, for web apps.