14 KiB
Database Schema
LinkLog stores its persistent state in SQLite. The schema is defined by the ordered migrations in backend/app/database.py, and the current schema version is 17 (PRAGMA user_version). Application startup applies migrations that are newer than the database's current version; existing migration entries must not be changed.
The default database file is backend/data/linklog.db. Set LINKLOG_DATABASE_PATH to use another path. Foreign-key enforcement is enabled for every application connection.
Entity-relationship diagram
erDiagram
USERS ||--o{ TOKENS : authenticates
USERS ||--o{ LINKS : owns
USERS ||--o{ USER_PLUGIN_CONFIG : configures
USERS ||--o{ EMAIL_VERIFICATION_TOKENS : verifies
USERS ||--o{ PASSWORD_RESET_TOKENS : resets
USERS ||--o{ MASTODON_OAUTH_STATES : authorizes
USERS ||--o{ USER_EMAIL_ADDRESSES : has
USERS ||--o{ OTP_RECOVERY_CODES : recovers
USERS ||--o{ SECURITY_AUDIT_EVENTS : acts
USERS o|--o{ TAGS : creates
LINKS ||--o{ LINK_TAGS : classified_by
TAGS ||--o{ LINK_TAGS : classifies
USER_EMAIL_ADDRESSES ||--o{ EMAIL_ADDRESS_VERIFICATION_TOKENS : verifies
USERS {
TEXT id PK
TEXT username UK
TEXT email UK
TEXT password_hash
TEXT avatar_url
TEXT bio
INTEGER is_admin
INTEGER email_verified
TEXT otp_secret
INTEGER otp_enabled
TEXT created_at
TEXT updated_at
}
TOKENS {
TEXT id PK
TEXT user_id FK
TEXT token_hash UK
TEXT token_type
TEXT expires_at
TEXT device_id
TEXT token_family_id
TEXT created_at
INTEGER revoked
}
LINKS {
TEXT id PK
TEXT user_id FK
TEXT title
TEXT url
TEXT comment
TEXT timestamp
TEXT created_at
TEXT updated_at
INTEGER is_public
INTEGER mastodon_posted
TEXT mastodon_post_id
TEXT mastodon_posted_at
TEXT mastodon_post_ids
}
TAGS {
TEXT id PK
TEXT name UK
TEXT created_by FK
TEXT created_at
}
LINK_TAGS {
TEXT link_id PK_FK
TEXT tag_id PK_FK
}
PLUGINS {
TEXT id PK
TEXT name UK
TEXT version
INTEGER enabled
TEXT config
TEXT created_at
TEXT updated_at
}
USER_PLUGIN_CONFIG {
TEXT id PK
TEXT user_id FK
TEXT plugin_name
TEXT config
TEXT created_at
TEXT updated_at
}
EMAIL_VERIFICATION_TOKENS {
TEXT id PK
TEXT user_id FK
TEXT token_hash UK
TEXT expires_at
TEXT created_at
}
PASSWORD_RESET_TOKENS {
TEXT id PK
TEXT user_id FK
TEXT token_hash UK
TEXT expires_at
TEXT created_at
}
MASTODON_OAUTH_STATES {
TEXT id PK
TEXT user_id FK
TEXT state_hash UK
TEXT instance
TEXT client_id
TEXT client_secret
TEXT redirect_uri
TEXT expires_at
TEXT created_at
}
USER_EMAIL_ADDRESSES {
TEXT id PK
TEXT user_id FK
TEXT email UK
INTEGER verified
TEXT created_at
TEXT updated_at
}
EMAIL_ADDRESS_VERIFICATION_TOKENS {
TEXT id PK
TEXT email_address_id FK
TEXT token_hash UK
TEXT expires_at
TEXT created_at
}
OTP_RECOVERY_CODES {
TEXT id PK
TEXT user_id FK
TEXT code_hash UK
INTEGER used
TEXT created_at
TEXT used_at
}
SECURITY_AUDIT_EVENTS {
TEXT id PK
TEXT actor_id FK
TEXT action
TEXT target_type
TEXT target_id
TEXT outcome
TEXT details
TEXT created_at
}
APP_SETTINGS {
TEXT name PK
TEXT value
TEXT updated_at
}
UK means a unique constraint. PK_FK means the column participates in the composite primary key and is also a foreign key. SQLite stores timestamps as TEXT using its timestamp defaults. Boolean values are stored as INTEGER values (0 or 1). JSON configuration and Mastodon post ID lists are stored as TEXT.
Tables
users
The user account and profile table.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key, normally a UUID. | |
username |
TEXT | no | Unique public username. | |
email |
TEXT | no | Unique primary email address. | |
password_hash |
TEXT | no | Password hash; plaintext passwords are not stored. | |
avatar_url |
TEXT | yes | Stored avatar reference. | |
bio |
TEXT | yes | Profile biography. | |
is_admin |
INTEGER | no | 0 |
Administrator flag. |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
updated_at |
TEXT | no | CURRENT_TIMESTAMP |
Last update timestamp. |
email_verified |
INTEGER | no | 0 |
Whether the primary email is verified. |
otp_secret |
TEXT | yes | Encrypted TOTP secret when configured. | |
otp_enabled |
INTEGER | no | 0 |
Whether OTP is required at login. |
tokens
Access and refresh token records. Only token hashes are persisted. device_id and token_family_id support device binding, rotation, reuse detection, and family revocation.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
user_id |
TEXT | no | FK to users.id. |
|
token_hash |
TEXT | no | Unique stored token hash. | |
token_type |
TEXT | no | access |
Token category. |
expires_at |
TEXT | no | Expiration timestamp. | |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
revoked |
INTEGER | no | 0 |
Revocation flag. |
device_id |
TEXT | yes | Client/device identifier. | |
token_family_id |
TEXT | yes | Refresh-token family identifier. |
links
Saved links and their publication state. mastodon_post_ids is a JSON array stored as text; the older mastodon_post_id column remains for migration compatibility.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
user_id |
TEXT | no | FK to users.id. |
|
title |
TEXT | no | Saved page title. | |
url |
TEXT | no | Tracking-cleaned URL. | |
comment |
TEXT | yes | User comment. | |
timestamp |
TEXT | no | User-supplied or captured link time. | |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
updated_at |
TEXT | no | CURRENT_TIMESTAMP |
Last update timestamp. |
is_public |
INTEGER | no | 1 |
Public-feed visibility flag. |
mastodon_posted |
INTEGER | no | 0 |
Whether publication has occurred. |
mastodon_post_id |
TEXT | yes | Legacy single Mastodon post ID. | |
mastodon_posted_at |
TEXT | yes | Publication timestamp. | |
mastodon_post_ids |
TEXT | yes | JSON array of publication IDs. |
tags and link_tags
tags contains reusable labels. link_tags is the many-to-many join table between links and tags. Tag names are unique, and tags.created_by is nullable so a deleted creator does not remove the tag.
| Table | Columns | Constraints |
|---|---|---|
tags |
id TEXT, name TEXT, created_at TEXT, created_by TEXT |
Primary key id; unique name; created_by references users.id with ON DELETE SET NULL. |
link_tags |
link_id TEXT, tag_id TEXT |
Composite primary key (link_id, tag_id); both FKs cascade on delete. |
Index: idx_link_tags_tag_id supports reverse tag lookups. idx_tags_created_by supports creator-based tag management.
plugins
Installed plugin definitions and global plugin configuration. config is JSON text.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
name |
TEXT | no | Unique plugin name. | |
version |
TEXT | no | Plugin version. | |
enabled |
INTEGER | no | 1 |
Enablement flag. |
config |
TEXT | yes | Plugin JSON configuration. | |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
updated_at |
TEXT | no | CURRENT_TIMESTAMP |
Last update timestamp. |
user_plugin_config
Per-user plugin settings. The pair (user_id, plugin_name) is unique; plugin_name is a logical plugin identifier and is not a foreign key to plugins.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
user_id |
TEXT | no | FK to users.id. |
|
plugin_name |
TEXT | no | Plugin identifier. | |
config |
TEXT | yes | User-specific JSON configuration. | |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
updated_at |
TEXT | no | CURRENT_TIMESTAMP |
Last update timestamp. |
Verification, reset, and OAuth state tables
These tables store one-time or short-lived workflow state. Token and state values are persisted as hashes where applicable. All user-owned rows are deleted when their user is deleted.
| Table | Important columns | Foreign key / uniqueness |
|---|---|---|
email_verification_tokens |
id, user_id, token_hash, expires_at, created_at |
user_id -> users.id with ON DELETE CASCADE; unique token_hash. |
password_reset_tokens |
id, user_id, token_hash, expires_at, created_at |
user_id -> users.id with ON DELETE CASCADE; unique token_hash. |
mastodon_oauth_states |
id, user_id, state_hash, instance, client_id, client_secret, redirect_uri, expires_at, created_at |
user_id -> users.id with ON DELETE CASCADE; unique state_hash. OAuth client secrets are encrypted by the service layer. |
Indexes: idx_email_verification_tokens_user_id, idx_password_reset_tokens_user_id, and idx_mastodon_oauth_states_state_hash.
app_settings
Global key/value settings, including setup and mail configuration. Sensitive values are encrypted by the service layer before storage where required.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
name |
TEXT | no | Primary key setting name. | |
value |
TEXT | no | Setting value. | |
updated_at |
TEXT | no | CURRENT_TIMESTAMP |
Last update timestamp. |
user_email_addresses
Verified and pending alternative email addresses. The primary address remains in users.email; this table holds additional addresses.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
user_id |
TEXT | no | FK to users.id with ON DELETE CASCADE. |
|
email |
TEXT | no | Globally unique alternative address. | |
verified |
INTEGER | no | 0 |
Verification flag. |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
updated_at |
TEXT | no | CURRENT_TIMESTAMP |
Last update timestamp. |
Index: idx_user_email_addresses_user_id.
email_address_verification_tokens
Verification tokens for alternative addresses.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
email_address_id |
TEXT | no | FK to user_email_addresses.id with ON DELETE CASCADE. |
|
token_hash |
TEXT | no | Unique token hash. | |
expires_at |
TEXT | no | Expiration timestamp. | |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
Index: idx_email_address_verification_tokens_address_id.
otp_recovery_codes
One-time recovery codes for users with OTP enabled. Only code hashes are stored.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
user_id |
TEXT | no | FK to users.id with ON DELETE CASCADE. |
|
code_hash |
TEXT | no | Unique recovery-code hash. | |
used |
INTEGER | no | 0 |
Consumption flag. |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Creation timestamp. |
used_at |
TEXT | yes | Consumption timestamp. |
Index: idx_otp_recovery_codes_user_id.
security_audit_events
Security-relevant audit events. actor_id is nullable so an account deletion does not remove the audit record; it becomes NULL through ON DELETE SET NULL.
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id |
TEXT | no | Primary key. | |
actor_id |
TEXT | yes | FK to users.id, ON DELETE SET NULL. |
|
action |
TEXT | no | Action name. | |
target_type |
TEXT | no | Target entity type. | |
target_id |
TEXT | yes | Target identifier. | |
outcome |
TEXT | no | success |
Result classification. |
details |
TEXT | no | {} |
JSON text, sanitized by the audit service. |
created_at |
TEXT | no | CURRENT_TIMESTAMP |
Event timestamp. |
Indexes: idx_security_audit_events_created_at and idx_security_audit_events_actor_id.
Migration history
| Version | Change |
|---|---|
| 1 | Creates users, tokens, links, plugins, and per-user plugin configuration. |
| 2 | Adds tags and the link/tag join table. |
| 3 | Normalizes tag names with a leading #. |
| 4 | Adds tag ownership through tags.created_by. |
| 5 | Adds primary-email verification and its token table. |
| 6 | Adds global application settings. |
| 7 | Adds password-reset tokens. |
| 8 | Adds Mastodon OAuth state. |
| 9-10 | Adds Mastodon publication fields and migrates to a JSON list of post IDs. |
| 11 | Adds OTP secret and enablement fields to users. |
| 12 | Adds alternative email addresses and their verification tokens. |
| 13-14 | Temporarily adds then removes pending primary-email-change state. It is not part of the current schema. |
| 15 | Adds device and token-family fields to tokens. |
| 16 | Adds OTP recovery codes. |
| 17 | Adds security audit events. |
To inspect a live database directly:
sqlite3 backend/data/linklog.db '.schema'
sqlite3 backend/data/linklog.db 'PRAGMA user_version;'