postguard-website
GitHub · SvelteKit · Web Application
Lives at apps/website in the postguard-js workspace. The standalone postguard-website repository is archived.
The PostGuard web frontend for encrypting and sending files. Users pick files, choose recipients by email address, authenticate with Yivi, and the files are encrypted and uploaded to Cryptify for delivery. Built with SvelteKit using the static adapter.
Integration
The website uses @e4a/pg-js with two Vite plugins for WASM support (vite-plugin-wasm and vite-plugin-top-level-await).
apps/website carries two git submodules, used by the local dev stack rather than by the built site: cryptify and postguard. Initialise them with:
git submodule update --init --recursiveNote that the cryptify submodule still points at encryption4all/cryptify, which is archived — the service is a member of the postguard workspace now, so that pin can never advance. Tracked in encryption4all/postguard-js#225.
The Thunderbird .xpi is not a submodule. scripts/sync-addons.mjs mirrors the add-on release artifacts into static/downloads/ — the .xpi from the monorepo's tb-addon-v* releases and the Outlook manifest.xml from its outlook-addin-v* releases — refreshing on a 6h interval inside the container.
For a step-by-step example of building a web application with PostGuard, see the pg-sveltekit example, which follows the same patterns as this website.
Recipient URL forms
The /decrypt and /download pages each accept a different URL shape depending on which envelope tier pg-js's createEnvelope() emitted on the sender side.
| Tier | Encryption mode | URL emitted in body | What the page does |
|---|---|---|---|
| 1 (small) | any | /decrypt#<urlsafe-base64> | Decodes the fragment in-browser, builds a ReadableStream of the ciphertext, and runs the inner MIME envelope through the fallback decrypter. |
| 2/3 | data | /decrypt?uuid=<id> | Calls pg.open({ uuid }) to fetch the ciphertext from Cryptify, decrypts, then parses the inner MIME with postal-mime. |
| 2/3 | files | /download?uuid=<id> | Calls pg.open({ uuid }) to fetch the ciphertext from Cryptify and surfaces the contained files for download. |
Both query-string forms accept an optional ?recipient=<key> hint. When the value matches one of the recipients in the encryption policy, the page skips the recipient picker and authenticates against that key directly.
// Path 2: ?uuid=… points at a Cryptify-uploaded ciphertext (tier
// 2/3 messages from pg-js >= 1.1.0 in `data: mime` mode). The
// Decrypt component accepts a uuid prop and calls pg.open({ uuid })
// to fetch + decrypt; the parsed plaintext is treated as RFC 5322
// MIME, so attachments and the inner body surface by name (matches
// the receive-side path the Outlook/TB add-ons take).
const params = new URLSearchParams(window.location.search)
const uuidParam = params.get('uuid')
const recipientParam = params.get('recipient')
if (uuidParam) {
uuid = uuidParam
recipient = recipientParam ?? undefined
hashMode = true
unique = {}
currRight = RIGHTMODES.Decrypt
}Source: src/routes/(app)/decrypt/+page.svelte#L86-L106
Download confirmation gate
Since encryption4all/postguard-website#258, the /download page decrypts into in-memory blobs and pauses on a confirmation step before any file is written to disk. The recipient sees the file list and the verified sender, then chooses to keep or discard the files. Declining drops the blobs without writing them; accepting starts the browser downloads. When the sender disclosed nothing beyond their email, the confirmation panel warns that email alone is a weak identity claim, since anyone with control of that mailbox could have signed the message.
Development
Quick Start with Docker Compose (recommended)
Docker Compose sets up everything: the PostGuard website, Cryptify file share server, IRMA server, PKG server, and a Mailcrab mail testing UI.
# Initialize submodules (Cryptify, etc.)
git submodule update --init --recursive
# Start all services with hot reload
docker-compose up
# Website: http://localhost:8080
# Mail UI: http://localhost:1080Your code changes reload automatically since the source is mounted as a volume.
Production Environment
docker-compose -f docker-compose.prod.yml up
# Access at http://localhostStopping Services
# Development
docker-compose down
# Production
docker-compose -f docker-compose.prod.yml downBuilding
Building is done automatically through GitHub Actions. You can also build manually:
docker-compose build # Build via Docker
npm run build # Build only the PostGuard websiteManual (without Docker)
npm install
npm run dev # dev server
npm run build # build SPA
npm run preview # preview production buildTesting
npm run check # Svelte type checking
npm run lint # Prettier + ESLint
npm run format # auto-format
npm run test # Playwright testsMobile Debugging
To test on a physical Android device, connect the phone with USB debugging enabled and make sure Yivi is in developer mode:
adb reverse tcp:8088 tcp:8088 # Yivi / IRMA server (for scanning QR codes)
adb reverse tcp:8080 tcp:8080 # PostGuard websiteEnvironment Variables
VITE_* variables are read at build time and baked into the bundle.
| Variable | Default | Description |
|---|---|---|
VITE_FILEHOST_URL | http://localhost:8000 | Cryptify file hosting service URL |
VITE_PKG_URL | http://localhost:8087 | PKG service URL |
VITE_MAX_UPLOAD_SIZE | none | Maximum file upload size in bytes |
VITE_UPLOAD_CHUNK_SIZE | none | Upload chunk size in bytes |
VITE_FILEREAD_CHUNK_SIZE | none | File read chunk size in bytes |
Runtime config
A second tier of flags is read at page load from a global APP_CONFIG object served by static/config.js. Deployed environments overwrite the file via the Terraform ConfigMap; local Docker Compose uses the in-repo copy. Each key is optional, with a fallback baked into src/lib/env.ts.
| Key | Type | Default | Description |
|---|---|---|---|
SITE_URL | string | https://postguard.eu | Base URL used by sitemap, RSS, JSON-LD, and the (app) routes. Marketing pages are prerendered, so the baked-in fallback is what ships in their static HTML; a runtime override only takes effect on non-prerendered routes such as /decrypt and /fileshare. Added in encryption4all/postguard-website#255. |
BUSINESS_URL | string | https://business.postguard.eu | Link target for the "for Business" entry point. |
FF_BUSINESS | boolean | false | Feature flag that controls whether the business entry point is shown. |
STAGING | boolean | false | True on staging/dev where cryptify runs with staging_mode = true and does not send notification mail. The website renders an in-page preview of the would-be recipient email so developers can grab the download link without trawling cryptify logs. Added in encryption4all/postguard-website#244. |
GLITCHTIP_DSN | string | "" | DSN for a Sentry-compatible error reporter (PostGuard runs GlitchTip). An empty value disables reporting at module load. The fileshare flow's CrashReport panel uses this to POST captured exceptions on user opt-in. Added in encryption4all/postguard-website#247. |
The runtime tier exists because these values change per environment without rebuilding the static bundle. Override them by replacing config.js in the served container.
Releasing
Releases come out of the postguard-js workspace via .github/workflows/website.yml. Versioning is changesets, not release-please, and the standalone postguard-website repository is archived and releases nothing.
The image is ghcr.io/encryption4all/postguard-website — the same GHCR package name the standalone repo published, hardcoded in the workflow rather than derived from the github.repository context (which resolves to postguard-js) precisely so postguard-ops' pin keeps working. Publishing happens on main only; PRs build without pushing.
CI/CD
Workflow: .github/workflows/website.yml in postguard-js. Deliberately not path-filtered to apps/website/**, so an SDK change in packages/pg-js is tested against the site in the same PR.
| Trigger | What it does |
|---|---|
PR / push to main | Svelte type checks, lint, CSS custom-property lint, unit tests, and an nginx config syntax test over both docker/nginx.dev.conf and docker/default.conf.template |
Push to main | Multi-arch Docker build pushed to GHCR (:edge, plus the changesets version on a release commit) |