Free, open-source image annotation and compositing.
Annotate screenshots and compose multi-image figures, directly in the browser, with no data ever leaving your machine.
Built for people who work with confidential materials: internal bug reports, hardware inspections, failure analyses, architecture diagrams, or anything else that shouldn't end up on someone else's server.
Stift focuses on the essential tools you need to get the job done, without the complexity or cost of commercial alternatives. It's not trying to be Photoshop. It's trying to be the fastest path from "I need to annotate this screenshot" to "here's the report-ready image."
More examples (click to expand)
| Bug Report | PCB Inspection |
|---|---|
| Weld Cross-Section Analysis | Bridge Inspection |
|---|---|
| Solar Array Inspection |
|---|
All examples are built-in and can be loaded from the onboarding screen.
Made in Germany 🇩🇪.
Licensed under the European Union Public Licence (EUPL-1.2).
Privacy first. Open source.
Annotation tools (16 tools, each with a single-key shortcut):
- Arrows (single/double-head, solid/dashed/dotted), text labels, text boxes with auto-sizing borders
- Highlight regions, blur/pixelate sensitive areas
- Rectangles, ellipses, lines (solid/dashed/dotted), freehand drawing
- Auto-incrementing numbered counters with optional tapered tails
- Dimension/measurement lines with unit calibration
- Stamp/watermark annotations (DRAFT, APPROVED, REJECTED, or custom text)
- Color boxes for redaction
- Connectors linking overview regions to detail images
- Eyedropper (pick any color from the canvas)
- Magnifier (draw a source region, get an enlarged inset with connecting line)
- Font selection (sans-serif, serif, monospace, Arial, Georgia)
Professional output:
- Auto-sized text boxes with uniform padding and colored borders
- Drop shadows, rounded corners, consistent visual style
- Dark counter badges with colored rings
- Dimension lines with end caps and labeled measurements
Image manipulation:
- Crop, rotate, brightness/contrast adjustment
- Lock images to prevent accidental displacement
- Position, size, and rotation via numerical inputs or handles
Canvas & workflow:
- Space+drag to pan, scroll to zoom, zoom-to-fit, pinch-to-zoom (touch)
- Snap-to-grid, canvas size control, background color (white/dark/transparent)
- Multi-select (Shift+click), Ctrl+A, arrow key nudge (1px / Shift+10px)
- Group/ungroup annotations (Ctrl+G / Ctrl+Shift+G), lock annotation position
- Shift-constrain: 15-degree angle snap for lines, square/circle for shapes, proportional resize
- Curved bezier arrows with draggable control point
- Copy/paste (Ctrl+C/V), duplicate (Ctrl+D), layer ordering (]/[)
- Right-click context menu with alignment tools (align edges, distribute, center)
- Undo/redo (Ctrl+Z/Y), inline text editing (double-click)
- Auto-save every 30 seconds with crash recovery dialog
- Drag-and-drop
.stiftproject files to open
Storage & compression:
- Local save: full original resolution, no compression, no size limits
- Server save (optional): images auto-compressed to WebP, then end-to-end encrypted client-side before upload. See docs/SECURITY.md
Export (all generated in-browser, nothing uploaded):
- PNG (1x or 2x), JPG (high/medium quality), PDF (canvas, A4, Letter)
- Transparent PNG (set canvas background to transparent)
- LaTeX:
.png+.texwith tikz overlays using your document's native font
The only requirement is Docker.
git clone <repository-url>
cd stift
docker compose upOpen http://localhost:8080 in your browser. A guided onboarding walks you through all tools and lets you load example projects.
To use a different port:
PORT=9090 docker compose up| Port | Where | What |
|---|---|---|
8080 |
host (default) | The only port you connect to. Override with PORT env. |
80 |
inside the container | nginx, serves the SPA and proxies /api/* to the Node API |
3001 |
inside the container, 127.0.0.1-only |
Node API, never exposed; nginx fronts it |
The Node API binds 127.0.0.1 only and is reachable solely through nginx on the same container. The host port mapping (PORT:80) is the only knob; there is no separate API port to expose or firewall.
Stift is designed for use on internal / trusted networks (your LAN, a VPN, an office subnet). The default
docker-compose.ymlexposes the app on a plain HTTP port with no transport encryption, no rate limiting, and no abuse protection beyond the basic per-user quotas. That is appropriate for an internal tool.If you expose Stift to the public Internet, you MUST put it behind a reverse proxy that provides HTTPS. Visitors entering passwords over plain HTTP would expose them to anyone on the network path; without a proxy, the integrity of every cloud-saved (encrypted) project depends on the integrity of the connection it was uploaded over.
For public-Internet deployments, use the included docker-compose.proxied.yml, which puts Caddy in front of Stift with automatic HTTPS via Let's Encrypt:
# 1. Point your DNS at the host (A/AAAA record).
# 2. Make sure ports 80 and 443 are reachable from the public Internet.
# 3. Create a .env file:
cat > .env <<EOF
STIFT_DOMAIN=stift.example.com
[email protected]
ALLOW_REGISTRATION=false
EOF
# 4. Bring it up:
docker compose -f docker-compose.proxied.yml up -d --buildThat's it. Caddy will provision and renew Let's Encrypt certificates automatically, redirect HTTP to HTTPS, set HSTS / CSP / X-Frame-Options, and proxy everything to the Stift container on the internal docker network. The Stift container itself is not exposed on any host port; Caddy is the only entry point.
The bundled Caddyfile includes a strict Content-Security-Policy that locks the SPA down to first-party resources only. Stift makes no third-party requests, and the CSP enforces that as a hard rule.
For other reverse proxies (Traefik, nginx, HAProxy) the same principle applies: bind only the proxy to the public IP, keep the Stift container on a private docker network, terminate TLS at the proxy, and proxy through to the Stift container's port 80.
Stift has no runtime dependency on the internet:
-
Build and export the Docker image:
docker compose build docker save image-annotation-stift:latest | gzip > stift-image.tar.gz
-
Transfer to the air-gapped machine via approved media.
-
Load and start:
docker load < stift-image.tar.gz docker compose up -dThis starts a permanent background service (
restart: unless-stopped). Projects are stored in./data/on the host.
The full documentation is split across topic-specific files in docs/:
| File | Topic |
|---|---|
| SECURITY.md | Privacy model, end-to-end encryption design, post-quantum analysis, comparison to audited password managers |
| ARCHITECTURE.md | Component diagram, SQLite + filesystem storage layout, users table schema |
| CONFIGURATION.md | All environment variables, locking down a public instance, invitation tokens, dev mode |
| DEVELOPMENT.md | Local dev, testing (unit / visual / E2E), built-in examples |
| KEYBOARD.md | All keyboard shortcuts |
Licensed under the European Union Public Licence (EUPL) v1.2.
You may use, modify, and redistribute this software under the terms of the EUPL. If you modify and distribute the software (including running a modified version as a network service), you must make the source code of your modifications available under the EUPL or a compatible licence.
The EUPL is legally valid in all EU member states and available in all 23 official EU languages.