nmemmertandClaude Sonnet 4.6 84ff218a30
Build & Release / Docker / backend (push) Successful in 1m7s
Build & Release / Docker / frontend (push) Successful in 1m14s
Build & Release / Desktop / Linux & Windows (push) Successful in 18m15s
Build & Release / Deploy to server (push) Successful in 18s
Build & Release / Create release (push) Has been cancelled
fix: support both docker compose (v2) and docker-compose (v1) in deploy
The target server has Docker Compose V1 (standalone binary) not the V2
plugin, so 'docker compose' fails. Detect which is available at runtime
and use the appropriate command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-28 11:27:03 -04:00

CloudLink

End-to-end encrypted messaging and cloud drive. The server stores only ciphertext — it cannot read messages or files under any circumstance.

Features

  • Zero-knowledge messaging — X25519 ECDH key exchange; AES-256-GCM encrypted messages
  • Encrypted drive — Files are encrypted in-browser before upload; server stores opaque blobs
  • Real-time delivery — WebSocket relay; server forwards ciphertext without decrypting
  • Two-factor authentication — TOTP (Google Authenticator, Authy, etc.) with backup codes
  • Progressive Web App — installable from the browser on any OS
  • Desktop app — native Electron wrapper for Mac, Windows, and Linux
  • Admin portal — manage users, storage quotas, and admin flags
  • CI/CD — Gitea Actions builds Docker images and desktop installers on every push

Architecture

Browser / Desktop app
        │  HTTPS + WSS
        ▼
   nginx (TLS termination, reverse proxy)
        │
   ┌────┴──────┐
   │  frontend │  React 18 SPA served by nginx:alpine
   └───────────┘
        │  /api/* and /ws proxied
        ▼
   ┌────────────┐
   │  backend   │  Express + TypeScript, port 3001
   └─────┬──────┘
         │
   ┌─────▼──────┐
   │ PostgreSQL  │  pg:16-alpine, persistent volume
   └────────────┘

All encryption and decryption happens exclusively in the browser/client. The backend is a dumb relay and storage layer.


Encryption Model

Keys

Key Algorithm Where it lives
Password-derived key PBKDF2-SHA256 (600 000 iterations) In-memory only, derived at login
Master key AES-256-GCM Encrypted with derived key, stored on server
X25519 keypair X25519 ECDH Private key encrypted with master key, stored on server; public key stored plaintext
Conversation key AES-256-GCM Per-conversation symmetric key, encrypted with each member's X25519 public key
File key AES-256-GCM Per-file symmetric key, encrypted with owner's master key

Message flow

  1. Alice opens a conversation with Bob
  2. Client generates a random AES-256-GCM conversation key
  3. Key is sealed with Bob's X25519 public key (ephemeral ECDH + HKDF + AES-GCM)
  4. Ciphertext of the key is stored in conversation_members.encrypted_key
  5. Each message is encrypted with the conversation key
  6. Server stores and relays only ciphertext — it never sees plaintext

File flow

  1. Client generates a random AES-256-GCM file key
  2. File is encrypted in-browser with the file key
  3. File key is encrypted with the user's master key
  4. Encrypted file and encrypted file key are uploaded; filename is also encrypted
  5. Server stores opaque bytes at a UUID-based path

Tech Stack

Backend

  • Node.js 20 + TypeScript
  • Express 4, Helmet, express-rate-limit
  • ws WebSocket server
  • PostgreSQL 16 with pgcrypto
  • Argon2id for password hashing
  • jsonwebtoken — 15-min access tokens + 30-day refresh tokens
  • otplib for TOTP 2FA
  • multer for file uploads
  • zod for request validation

Frontend

  • React 18 + Vite + TypeScript
  • Tailwind CSS
  • Zustand (auth + crypto keys in-memory store)
  • TanStack Query v5
  • React Router v6
  • vite-plugin-pwa (PWA / installable)
  • Web Crypto API (no external crypto libraries)
  • lucide-react icons
  • date-fns

Desktop

  • Electron 33
  • electron-store (persistent server URL)
  • electron-builder (Mac DMG, Windows NSIS, Linux AppImage)

Infrastructure

  • Docker + Docker Compose
  • nginx (TLS, reverse proxy, HSTS, security headers)
  • Gitea Actions (CI/CD)

Running Locally (Development)

Prerequisites

  • Node.js 20+
  • Docker + Docker Compose

1. Clone and configure

git clone https://git.necloud.us/nate/cloudlink.git
cd cloudlink
cp .env.example .env   # edit values

.env minimum:

POSTGRES_DB=cloudlink
POSTGRES_USER=cloudlink
POSTGRES_PASSWORD=changeme
JWT_SECRET=change-me-at-least-32-chars
JWT_REFRESH_SECRET=change-me-at-least-32-chars
MAX_FILE_SIZE_MB=100

2. Start the database

docker compose up -d postgres

3. Run the backend

cd backend
npm install
npm run dev        # http://localhost:3001

4. Run the frontend

cd frontend
npm install
npm run dev        # http://localhost:5173

Set VITE_API_URL=http://localhost:3001/api and VITE_WS_URL=ws://localhost:3001/ws in frontend/.env.local.


Production Deployment (Docker)

Prerequisites

  • Docker + Docker Compose on the server
  • SSL certificate at nginx/ssl/fullchain.pem and nginx/ssl/privkey.pem

Option A — Build locally, run on server

docker compose build
docker compose up -d

After a successful Gitea Actions run:

echo "$REGISTRY_TOKEN" | docker login git.necloud.us -u nate --password-stdin
docker compose pull backend frontend
docker compose up -d --no-build

Environment variables (production .env)

POSTGRES_DB=cloudlink
POSTGRES_USER=cloudlink
POSTGRES_PASSWORD=<strong-password>
JWT_SECRET=<64-char-random-string>
JWT_REFRESH_SECRET=<64-char-random-string>
HOST_PORT=443
MAX_FILE_SIZE_MB=100

Generate strong secrets:

openssl rand -hex 32

First admin user

After the stack is up, register a normal account, then promote it:

docker exec -i cloudlink-postgres-1 psql -U cloudlink -d cloudlink \
  -c "UPDATE users SET is_admin = true WHERE email = 'you@example.com';"

Subsequent admins can be toggled from the Admin portal in the UI.


Desktop App

Install pre-built (from Gitea release)

Download the installer for your OS from Releases:

OS File Install
Windows CloudLink Setup.exe Run the installer, follow wizard
Linux CloudLink.AppImage chmod +x CloudLink.AppImage && ./CloudLink.AppImage
Mac CloudLink.dmg Open DMG, drag to Applications (built locally — see below)

On first launch the app prompts for your CloudLink server URL (e.g. https://cloudlink.example.com). This is saved and used for all future launches.

Build locally

cd desktop
npm install

npm run build:mac    # requires macOS
npm run build:win    # requires macOS or Linux (Wine)
npm run build:linux  # any platform

Outputs go to desktop/dist/.


Admin Portal

Accessible at /admin for users with is_admin = true.

  • View all users, registration date, last seen
  • Per-user storage usage vs quota (colour-coded bar)
  • Inline quota editing (MB)
  • Toggle admin flag per user
  • Aggregate stats (total users, total storage, total files)

CI/CD (Gitea Actions)

Workflow: .gitea/workflows/docker-build.yml

Job Trigger Action
build-backend every push / PR Build + push backend:latest and backend:<sha> to Gitea registry
build-frontend every push / PR Build + push frontend:latest and frontend:<sha>
build-desktop every push / PR Build Linux AppImage + Windows NSIS; upload as workflow artifacts (30-day retention)
deploy push to master SSH into server, pull new images, restart containers
release push of a v* tag Create Gitea release, attach desktop installers

Required secrets (Repo → Settings → Secrets)

Secret Value
REGISTRY_USER Gitea username (e.g. nate)
REGISTRY_TOKEN Gitea API token with package:write scope
DEPLOY_HOST Server hostname or IP
DEPLOY_USER SSH user
DEPLOY_SSH_KEY SSH private key (contents of ~/.ssh/id_ed25519)
DEPLOY_PATH Absolute path to cloudlink on server (e.g. /opt/cloudlink)

Tagging a release

git tag v1.2.0 && git push origin v1.2.0

This triggers the release job which creates a Gitea release entry and attaches the built installers.

Runner setup

A Gitea act runner is required. Install on your server:

wget https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64 -O act_runner
chmod +x act_runner

# Get registration token from:
# Gitea → Site Administration → Actions → Runners → Create Runner

./act_runner register \
  --no-interactive \
  --instance https://git.necloud.us \
  --token <TOKEN> \
  --name cloudlink-runner \
  --labels ubuntu-latest:docker://node:20-bullseye

./act_runner daemon

The runner host must have Docker installed.


Security Notes

  • Crypto keys are never written to disk or localStorage — they live in Zustand's in-memory store and are lost on page reload (by design). Users re-derive them from their password at each login.
  • The server stores only: password_hash (Argon2id), kdf_salt, encrypted_master_key, encrypted_private_key, and ciphertext blobs. No plaintext content.
  • JWTs are short-lived (15 min). Refresh tokens are rotated on use and hashed before storage.
  • nginx enforces HSTS, X-Frame-Options, X-Content-Type-Options, and a strict CSP.
  • File paths on disk are UUID-based — not guessable from filenames.
  • Rate limiting is applied to auth endpoints.

Database Schema

See backend/src/db/schema.sql. Tables:

Table Purpose
users Account credentials, KDF salt, encrypted master key, 2FA state, quota
user_keys X25519 public key + encrypted private key
refresh_tokens Hashed refresh tokens with expiry
conversations Direct message threads
conversation_members Per-member encrypted conversation key
messages Ciphertext-only message records
files Encrypted file metadata + storage path

Repository

Source: https://git.necloud.us/nate/cloudlink
Container registry: git.necloud.us/nate/cloudlink/{backend,frontend}:latest

S
Description
CloudLink end-to-end encrypted messaging and cloud drive. The server stores only ciphertext — it cannot read messages or files under any circumstance.
Readme
536 KiB
0 Stars 1 Watchers 0 Forks
2026-08-31 15:59:06 +00:00
Languages
TypeScript 91.2%
JavaScript 7%
CSS 0.8%
Dockerfile 0.6%
HTML 0.4%