← Back to skwod

Documentation

Everything you need to deploy, configure, and run skwod.

Getting Started

skwod is a private communication platform designed for small groups (<10 users). Think Discord, but self-hosted, encrypted, and entirely under your control. It includes text chat, voice & video channels, stories, file sharing, roles, and permissions.

The fastest way to get started is with the CLI:

npx skwod init

This will scaffold a new skwod instance, walk you through environment setup, and spin up Docker containers. You can also set things up manually — see the Self-Hosting section below.

Architecture

skwod is a monorepo managed with Turborepo and pnpm workspaces. The main packages are:

  • apps/web Next.js 15 App Router web client with React 19 and Tailwind CSS v4
  • apps/mobile Expo SDK 52 / React Native mobile client with NativeWind
  • packages/shared — TypeScript types, Zod validators, bigint bitmask permissions, constants
  • packages/supabase — Supabase client factory, database types, realtime helpers
  • supabase/ — Supabase CLI project with migrations, edge functions, and seed data

Build order is packages/shared packages/supabaseapps/*. Turborepo resolves the dependency graph automatically.

Self-Hosting

Docker Compose

The recommended way to self-host skwod. A single docker-compose.yml brings up the web app, Supabase stack, and all dependencies.

git clone https://github.com/ahmedhosny/skwod.git
cd skwod
cp .env.example .env
# Edit .env with your keys
docker compose up -d

VPS Deployment

Any VPS with Docker will work. Popular choices include DigitalOcean, Hetzner, and Linode. A small instance (2 GB RAM, 1 vCPU) is enough for a group of 10.

# Example: spin up on a fresh Ubuntu VPS
sudo apt update && sudo apt install -y docker.io docker-compose-plugin
git clone https://github.com/ahmedhosny/skwod.git
cd skwod
cp .env.example .env
docker compose up -d

Unraid

skwod is available as an Unraid Community Apps template. Search for "skwod" in the Community Applications plugin and follow the prompts to configure your environment variables.

Environment Variables

These variables are required for skwod to function. Set them in your .env file or your hosting provider's environment configuration.

# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...

# LiveKit (voice & video)
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=API...
LIVEKIT_API_SECRET=secret...

Cloudflare Tunnel

To expose your self-hosted instance to the internet without opening ports, use Cloudflare Tunnel. It creates a secure outbound-only connection from your server to Cloudflare's edge, giving you HTTPS, DDoS protection, and a custom domain with zero configuration on your firewall.

# Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

# Authenticate and create tunnel
cloudflared tunnel login
cloudflared tunnel create skwod
cloudflared tunnel route dns skwod chat.yourdomain.com

# Run it
cloudflared tunnel run skwod

Services & Infrastructure

skwod relies on a handful of external services. All are free-tier friendly for small groups.

  • Supabase — Auth, Postgres database, Realtime subscriptions, Storage (file uploads), and Edge Functions. The entire backend.
  • LiveKit — WebRTC-based voice and video infrastructure. Provides low-latency, scalable media servers with E2EE support.
  • Docker — Container runtime for self-hosting. The docker-compose.yml orchestrates all services.
  • Cloudflare Tunnel — Secure tunneling to expose your instance to the internet without port forwarding.
  • TUS Protocol — Open protocol for resumable file uploads. Used for all file and attachment uploads in skwod.

Database

skwod uses Postgres via Supabase. The schema is managed through SQL migration files in supabase/migrations/.

  • Migrations run automatically on supabase db push or during Docker startup.
  • Row Level Security (RLS) policies enforce access control at the database level — every table has policies that restrict reads and writes to authorized users.
  • Bigint bitmask permissions (defined in packages/shared) map to roles and channels, checked both client-side and in RLS policies.

Realtime

Supabase Realtime powers all live features over WebSocket connections:

  • Live messages — New messages, edits, and deletions broadcast instantly to all connected clients in a channel.
  • Typing indicators — Presence-based typing status shows who is currently composing a message.
  • Presence — Online/offline status for all server members, synced across devices.
  • Reactions & threads — Live updates for message reactions and thread replies.

Edge Functions

Supabase Edge Functions (Deno-based) handle server-side logic that runs close to the user:

  • Push notifications — Sends push notifications for new messages and DMs via APNs and FCM.
  • Link previews — Fetches Open Graph metadata to render rich previews for shared URLs.
  • E2EE key management — Handles device registration, pre-key bundle distribution, sender key distribution, and key backup/restore for the encryption system.

End-to-End Encryption

skwod implements a full end-to-end encryption stack modeled on the Signal Protocol — the same cryptographic foundation used by Signal, WhatsApp, and Google Messages. All user-written content — channel messages, DMs, feed posts, feed comments, and stories — is encrypted on the sender's device and can only be decrypted by intended recipients. The server never sees plaintext content, and cannot forge sender identities.

The implementation lives in packages/shared/src/crypto/ with platform-specific wrappers in apps/web and apps/mobile. The shared crypto library runs in the browser (Web Crypto API + libsodium), React Native (libsodium-wrappers-sumo), and Deno (edge functions).

Protocol Stack

skwod's encryption is built from five protocol layers, each handling a specific concern:

LayerProtocolPurpose
Key ExchangePQXDH (X25519 + ML-KEM-1024)Establish shared secrets between device pairs
Session EncryptionDouble Ratchet + SPQRPer-message forward secrecy and post-quantum mixing
Metadata ProtectionSealed SenderHide sender identity from the server
Group MessagingSender Keys + PQ Group RatchetEfficient group encryption with post-quantum ratcheting
Key PersistenceArgon2id + AES-256-GCM BackupEncrypted cloud backup of all device keys

PQXDH Key Exchange

When two devices first communicate, they perform PQXDH (Post-Quantum Extended Diffie-Hellman) — the same key agreement protocol Signal shipped in 2023. PQXDH combines classical elliptic curve Diffie-Hellman with a post-quantum KEM (Key Encapsulation Mechanism) to produce a shared secret that is secure against both classical and quantum adversaries.

Key components

  • Identity keys — Long-term Ed25519 keypair (converted to X25519 for DH). Generated once per device and persisted locally.
  • Signed prekey (SPK) — Medium-term X25519 keypair, signed by the identity key. Rotated every 7 days with a 48-hour grace period for in-flight messages.
  • One-time prekeys (OPKs) — 100 single-use X25519 keypairs uploaded to the server. Each prekey is consumed exactly once and then deleted. Refilled when stock drops below 25.
  • ML-KEM-1024 prekey — Post-quantum lattice-based KEM key (NIST FIPS 203). Encapsulated shared secret is combined with the X25519 DH outputs so that an attacker must break both classical and post-quantum assumptions to recover the session key.

The PQXDH output is 96 bytes: rootKey(32) + chainKey(32) + pqRatchetKey(32). The pqRatchetKey feeds into SPQR for ongoing post-quantum message key mixing.

Double Ratchet

After PQXDH establishes a shared secret, all subsequent messages use the Double Ratchet algorithm. This provides two critical security properties:

  • Forward secrecy — Each message derives a unique encryption key. Compromising the current key does not reveal past messages.
  • Break-in recovery — Even if an attacker temporarily compromises the session state, future messages become secure again after the next DH ratchet step.

The ratchet advances with every message sent and performs a DH ratchet step on each reply, generating fresh X25519 keypairs. Messages can arrive out of order — up to 1,000 skipped message keys are cached per session, and chains are capped at 2,000 messages before forcing a DH ratchet.

SPQR — Post-Quantum Message Key Mixing

Every Double Ratchet message additionally passes through SPQR (Simplified Post-Quantum Ratchet). The pqRatchetKey derived during PQXDH is mixed into each message's key derivation using a random 32-byte salt carried in the message header. This means that even if the classical DH ratchet is broken by a quantum computer, every message remains protected by the ML-KEM-1024 shared secret.

SPQR is stateless — it does not affect message ordering or the ability to decrypt out-of-order messages.

Sealed Sender

In a standard encrypted messaging system, the server knows who is messaging whom even though it can't read message content. skwod's Sealed Sender protocol eliminates this metadata, so the server sees only the recipient — not the sender.

How it works

  • Generate an ephemeral X25519 keypair and perform ECDH with the recipient's identity key to derive a cipher key.
  • Encrypt the sender's identity under the ephemeral key (the server cannot read this).
  • Derive a static key from both the sender's identity key and the ephemeral key, then encrypt the inner message + sender certificate using AES-256-GCM-SIV.
  • The sealed envelope contains only the ephemeral public key, the encrypted sender identity, and the encrypted content. The server routes it by recipient but cannot identify the sender.

The recipient decrypts the ephemeral layer to discover who sent the message, then verifies the sender certificate before decrypting the content.

AES-256-GCM-SIV

The sealed sender content layer uses AES-256-GCM-SIV (RFC 8452) instead of standard AES-GCM. GCM-SIV is nonce-misuse resistant: if a nonce is accidentally reused, GCM-SIV degrades gracefully to deterministic encryption rather than catastrophically leaking the authentication key (as standard GCM would).

Version negotiation ensures backward compatibility: v2 envelopes use GCM-SIV, while legacy v1 envelopes are still decryptable via standard GCM.

Server Certificate Chain

Sealed sender requires the server to issue sender certificates that vouch for a user's identity. skwod uses a two-level certificate hierarchy:

  • Root signing key — A long-lived Ed25519 keypair derived from the server secret. Never exposed directly.
  • Server certificate — A 30-day Ed25519 keypair signed by the root key. Used to sign sender certificates. Clients verify the chain: root signature → server cert → sender cert.
  • Sender certificate — A 24-hour certificate binding a user ID + device ID + identity key. Signed by the current server certificate. Clients cache it for 23 hours and refresh 1 hour early.

All certificate signing uses canonical binary encoding (fixed-layout byte concatenation) rather than JSON serialization, eliminating non-determinism across JavaScript runtimes (V8, Hermes, Deno). A revocation list allows compromised server key IDs to be permanently rejected by clients.

Server Public Key Pinning

Clients verify that every sender certificate was genuinely issued by the server by checking it against the pinned server public key. If a certificate cannot be verified (e.g., the server public key is unavailable or the signature doesn't match), the message is rejected. This prevents an attacker who compromises the transport layer from forging sender certificates.

Identity Verification (Safety Numbers)

Users can verify each other's identity keys out-of-band using Safety Numbers — a 60-digit code (12 groups of 5 digits) derived from both users' identity keys. The computation uses 100,000 HMAC-SHA256 iterations per fingerprint half, making brute-force preimage attacks computationally infeasible.

When a user verifies a safety number, the verification is stored locally. If the remote user's identity key changes (e.g., they re-register a device), the verification is automatically cleared and a warning banner is shown.

Identity Key Change Protection

When a contact's identity key changes, skwod blocks outgoing messages until the user explicitly accepts the new key. This prevents a man-in-the-middle attack where an adversary substitutes their own key:

  • On encrypt — If the recipient's identity key doesn't match the stored key and the user hasn't accepted the change, encryption is blocked and a warning banner is shown.
  • On decrypt — Incoming messages from a contact with a changed key are still decrypted (to prevent message loss), but a warning banner alerts the user.
  • Accept flow — The user clicks "Accept" on the warning banner, which stores the new key and re-enables encryption. Previous safety number verification is cleared automatically.

Message Padding

All messages are padded using ISO 7816-4 padding to fixed 160-byte blocks before encryption. This prevents traffic analysis attacks where an observer could infer message content from ciphertext length (e.g., distinguishing "yes" from a long paragraph).

Sender Keys (Group Encryption)

Group channels use the Sender Key protocol for efficient group messaging. Instead of encrypting a message N times for N recipients, each sender maintains their own symmetric ratchet chain and distributes the initial key to all group members via pairwise Double Ratchet sessions.

  • Encrypt once — The sender encrypts once with their sender key. All group members can decrypt using the distributed key.
  • Key rotation on leave — When a member leaves, all remaining members rotate their sender keys so the departed member cannot decrypt future messages.
  • PQ Group Ratchet — Every 100 messages, the sender key chain mixes in a fresh ML-KEM shared secret for post-quantum forward secrecy in group chats.

Multi-Session Archival

When a session is replaced (e.g., a contact re-registers), the old session is archived rather than deleted. skwod keeps up to 40 archived sessions per device pair (matching Signal's limit) with a 30-day TTL. If a message fails to decrypt with the active session, archived sessions are tried in newest-first order. This handles race conditions during session renegotiation and device re-registration.

Key Backup & Recovery

Users can create an encrypted backup of all their device keys (identity key, signed prekey, sessions, sender keys) protected by a recovery passphrase.

  • Argon2id key derivation — The passphrase is stretched using Argon2id (OWASP 2024 parameters: t=2, 19 MiB memory) to derive a 256-bit encryption key. This makes brute-force attacks against the passphrase computationally expensive.
  • AES-256-GCM encryption — The key material is encrypted and stored server-side. The server stores only the ciphertext — it cannot derive the passphrase or read the keys.
  • Cross-device restore — On a new device, download the encrypted blob, enter the passphrase, and all keys are restored. Sessions resume without requiring contacts to re-verify.

Encrypted Attachments

Files and images are encrypted client-side before upload using AES-256-GCM with per-file random keys. Large files are processed in 64KB chunks with chunk-specific additional authenticated data (AAD) to prevent chunk reordering or truncation attacks. The encryption key is sent inline with the encrypted message, so the storage server never has access to file contents.

Multi-Device Support

Each user can register up to 5 devices. Every device has its own independent identity key, prekey bundle, and session state. Messages are fan-out encrypted: the sender encrypts separately for each of the recipient's devices using their individual pairwise sessions. A self-distribution mechanism keeps sender keys synchronized across a user's own devices.

Concurrency & Atomicity

All encrypt/decrypt operations are serialized per session key using an async mutex to prevent ratchet state corruption from concurrent operations. State updates use crypto transactions — if decryption fails, the session state is rolled back to its pre-operation state, preventing desynchronization.

Signal Protocol Comparison

skwod's encryption stack is modeled directly on the Signal Protocol. The table below compares specific implementation details between the two.

FeatureSignalskwod
Key ExchangePQXDH (X25519 + ML-KEM-768)PQXDH (X25519 + ML-KEM-1024)
Session EncryptionDouble Ratchet (AES-256-CBC + HMAC-SHA256)Double Ratchet (AES-256-GCM)
Post-Quantum RatchetPQXDH seeds initial ratchetPQXDH + SPQR (per-message PQ key mixing)
Sealed Sender EncryptionAES-CTR + HMAC-SHA256AES-256-GCM-SIV (nonce-misuse resistant)
Sender CertificatesServer-signed, 24h validityTwo-level chain (root → server cert → sender cert), 24h validity
Certificate EncodingProtobuf (deterministic)Canonical binary encoding (deterministic)
Certificate RevocationServer-side revocation listClient-side revoked key ID set + server cert expiration
Server Key PinningTrust On First UseDynamic pinning (fetched from certificate endpoint)
Safety Number Iterations5,200 HMAC-SHA512100,000 HMAC-SHA256
Safety Number Format60 digits (12 x 5)60 digits (12 x 5)
Identity Key ChangeBlocks sending, shows warningBlocks sending, shows warning, explicit accept required
Message PaddingPKCS#7 to next multiple of 160 bytesISO 7816-4 to next multiple of 160 bytes
Group MessagingSender Keys v2Sender Keys + PQ Group Ratchet (ML-KEM every 100 messages)
Archived SessionsUp to 40 per device pairUp to 40 per device pair, 30-day TTL
Max Skipped Keys~2,0001,000 per session
Max Chain LengthNo hard limit2,000 (forces DH ratchet step)
Key BackupSVR2 (Secure Value Recovery, Intel SGX)Argon2id (t=2, 19 MiB) + AES-256-GCM, server-stored ciphertext
Attachment EncryptionAES-256-CTR + HMAC-SHA256AES-256-GCM with 64KB chunked AAD
Multi-DeviceLinked devices with single identityIndependent device identities, fan-out encryption, up to 5 devices
Signed Prekey Rotation~2 days7 days with 48-hour fallback grace period
Crypto Librarylibsignal (Rust)libsodium-wrappers-sumo + @noble/ciphers (TypeScript)

Key differences: skwod uses ML-KEM-1024 (vs Signal's ML-KEM-768) for a larger post-quantum security margin, applies post-quantum key mixing to every message via SPQR (not just the initial handshake), and uses AES-256-GCM-SIV for sealed sender content to provide nonce-misuse resistance. The certificate infrastructure uses a two-level hierarchy with an explicit revocation mechanism.

Signal's implementation benefits from a Rust-based crypto library (libsignal) and hardware-backed key storage (SVR2 on Intel SGX). skwod trades these for a pure TypeScript stack that runs identically in browsers, React Native, and Deno edge functions — optimized for self-hosted deployments where the operator controls the full stack.

Voice & Video

Voice and video channels are powered by LiveKit Cloud. LiveKit provides a WebRTC-based SFU (Selective Forwarding Unit) that handles media routing, simulcast, and scalability.

Getting API keys

Sign up at cloud.livekit.io, create a project, and copy your API key, API secret, and WebSocket URL into your environment variables.

E2EE for calls

LiveKit supports end-to-end encryption for voice and video using insertable streams. When enabled, media is encrypted in the browser before being sent to the SFU — LiveKit routes the packets but cannot decrypt them. skwod includes the livekit-client.e2ee.worker.mjs Web Worker for client-side encryption.

Joining a Server

skwod servers are private by default. There are three ways to invite people:

  • Join codes — A short alphanumeric code that members can type in to join.
  • QR codes — Scannable from the mobile app for in-person invites.
  • Invite links — Shareable URLs in the format yourdomain.com/invite/CODE that take new users through signup and directly into the server.

Server admins can manage invites, set expiration, and revoke codes from the server settings panel.

Mobile App

The skwod mobile app is built with Expo SDK 52 and React Native. It's available for iOS and Android.

  • Connecting to your instance — On first launch, enter your skwod server URL. The app stores it and connects automatically on subsequent launches.
  • Push notifications — Powered by Expo push tokens and Supabase Edge Functions. You'll receive notifications for new messages and DMs even when the app is backgrounded.
  • Background audio — Voice channels continue playing when the app is in the background.
  • Feature parity — The mobile app supports all the same features as web: E2EE, voice & video, file uploads, stories, reactions, threads, and more.

Desktop App

The skwod desktop app is built with Electron and is available for macOS, Windows, and Linux.

  • System notifications — Native OS notifications for messages and calls.
  • Menu bar / system tray — Quick access to your server without keeping a browser tab open.
  • Auto-updates — The app checks for updates on launch and installs them in the background.
  • Cross-platform — The same codebase runs on macOS (Apple Silicon & Intel), Windows (x64 & ARM), and Linux (AppImage, deb).