Skip to content

Database Schema

SkySend uses SQLite with Drizzle ORM. The database file is located at data/skysend.db.

SQLite Configuration

sql
PRAGMA journal_mode = WAL;        -- Concurrent reads + serialized writes
PRAGMA busy_timeout = 5000;       -- Wait up to 5s on lock contention
PRAGMA synchronous = NORMAL;      -- Safe with WAL, better write performance
PRAGMA foreign_keys = ON;         -- Enforce referential integrity

WAL (Write-Ahead Logging) mode allows concurrent reads while writes are serialized. This is more than sufficient for a single-instance self-hosted service.

Tables

uploads

ColumnTypeDefaultDescription
idTEXT (PK)-UUID v4
ownerTokenTEXT NOT NULL-Base64url-encoded owner token
authTokenTEXT NOT NULL-Base64url-encoded auth token
saltBLOB NOT NULL-HKDF salt (16 bytes)
encryptedMetaBLOBNULLAES-256-GCM encrypted metadata
nonceBLOBNULLMetadata IV (12 bytes)
sizeINTEGER NOT NULL-Total payload size in bytes
fileCountINTEGER1Number of files (1 = single, >1 = archive)
hasPasswordINTEGER0Whether password protection is active
passwordSaltBLOBNULLPassword KDF salt (16 bytes)
passwordAlgoTEXTNULL"argon2id" or "pbkdf2"
maxDownloadsINTEGER NOT NULL-Maximum allowed downloads
downloadCountINTEGER0Current download count
expiresAtTIMESTAMP NOT NULL-Expiry timestamp (Unix epoch)
createdAtTIMESTAMPcurrent_unix_timeCreation timestamp
storagePathTEXT NOT NULL-Filename on disk (UUID.bin)

Indexes

IndexColumnPurpose
idx_uploads_expires_atexpiresAtEfficient cleanup queries

notes

ColumnTypeDefaultDescription
idTEXT (PK)-UUID v4
ownerTokenTEXT NOT NULL-Base64url-encoded owner token
authTokenTEXT NOT NULL-Base64url-encoded auth token
saltBLOB NOT NULL-HKDF salt (16 bytes)
encryptedContentBLOB NOT NULL-AES-256-GCM encrypted note content
nonceBLOB NOT NULL-AES-GCM IV (12 bytes)
contentTypeTEXT NOT NULL-"text", "password", "code", "markdown", or "sshkey"
hasPasswordINTEGER NOT NULL0Whether password protection is active
passwordSaltBLOBNULLPassword KDF salt (16 bytes)
passwordAlgoTEXTNULL"argon2id" or "pbkdf2"
maxViewsINTEGER NOT NULL-Maximum allowed views (0 = unlimited)
viewCountINTEGER NOT NULL0Current view count
expiresAtTIMESTAMP NOT NULL-Expiry timestamp (Unix epoch)
createdAtTIMESTAMP NOT NULLcurrent_unix_timeCreation timestamp

Indexes (notes)

IndexColumnPurpose
idx_notes_expires_atexpiresAtEfficient cleanup queries

Concurrency

Download and view count updates are atomic SQL operations that hold the write lock for microseconds. WAL mode allows thousands of such writes per second while reads are never blocked.

The download route uses an atomic SQL update with a WHERE condition to prevent race conditions:

sql
UPDATE uploads
SET downloadCount = downloadCount + 1
WHERE id = ? AND downloadCount < maxDownloads

The note view route uses the same pattern:

sql
UPDATE notes
SET view_count = view_count + 1
WHERE id = ? AND (max_views = 0 OR view_count < max_views)

If the limit is already reached, the update affects zero rows and the request is rejected. For notes with max_views = 0 (unlimited), the view count is always incremented.

Migrations

Database migrations are managed by Drizzle ORM and stored in apps/server/src/db/migrations/. Migrations run automatically on server startup.