The sync backbone for SNAP — a universal clipboard vault with a zero-knowledge architecture. The server handles authentication, encrypted clip synchronisation, device management, real-time plan events via SSE, and billing webhooks. It never holds decryption keys: all clipboard data is AES-256-GCM encrypted on the client before it reaches the wire.
- Zero-knowledge sync — server stores only ciphertext; encryption and decryption happen entirely on the client
- JWT auth — access + refresh token pair, with explicit logout / token invalidation
- Encrypted clip sync — pull with cursor-based pagination, push (Pro plan only)
- Device management — register, list, and revoke trusted devices
- SSE plan events — real-time push of plan change notifications to connected clients
- Rate limiting — Flask-Limiter, backed by Redis in production
- Structured logging — structlog with optional Sentry error tracking
- Alembic migrations —
render_as_batchenabled for full SQLite compatibility in development - Docker-first — dev compose stack included; production compose stack with Nginx also provided
| Layer | Choice |
|---|---|
| Framework | Flask 3.0.3 |
| Language | Python 3.12 |
| ORM | SQLAlchemy 2.x |
| Migrations | Flask-Migrate (Alembic) |
| Auth tokens | PyJWT |
| Dev database | SQLite |
| Prod database | MySQL (PyMySQL driver) |
| Rate limiting | Flask-Limiter |
| Logging | structlog |
| Error tracking | Sentry SDK (optional) |
| Cache / rate-limit backend | Redis (optional, prod) |
- Python 3.12+
- pip
- Docker & Docker Compose (for the container workflow)
- Redis (optional — only required for the production rate-limit backend)
git clone https://github.com/coderbenny/snap_BE
cd snap_BEpython -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activatepip install -r requirements.txtCopy the example env file and fill in your values:
cp .env.example .envSee the Environment Variables table below for all available options. At minimum you need FLASK_SECRET_KEY and JWT_SECRET.
flask db upgradeThis applies all Alembic migrations. SQLite is used by default — no additional setup required.
flask run --host=0.0.0.0 --port=5559The API is now available at http://localhost:5559.
Bring up the full dev stack (Flask + SQLite volume):
docker compose up --buildThe server is exposed on port 5559. Hot-reload is enabled via a bind mount.
To stop and remove containers:
docker compose down| Variable | Required | Default | Description |
|---|---|---|---|
FLASK_SECRET_KEY |
Yes | — | Secret key for Flask session signing |
JWT_SECRET |
Yes | — | Secret used to sign JWT access and refresh tokens |
DATABASE_URL |
No | sqlite:///snap.db |
SQLAlchemy database URL. Use mysql+pymysql://... for MySQL |
SENTRY_DSN |
No | — | Sentry DSN for error tracking. Omit to disable Sentry |
REDIS_URL |
No | — | Redis connection URL. Used as the Flask-Limiter backend in production. Omit to fall back to in-memory limiting |
FLASK_ENV |
No | development |
Set to production to enable production-mode behaviour |
Full request/response schemas are documented in openapi.yaml at the repository root.
| Method | Route | Description |
|---|---|---|
POST |
/auth/register |
Create a new user account |
POST |
/auth/login |
Authenticate and receive access + refresh tokens |
POST |
/auth/refresh |
Exchange a valid refresh token for a new access token |
POST |
/auth/logout |
Invalidate the current refresh token |
| Method | Route | Plan | Description |
|---|---|---|---|
GET |
/sync/pull |
Free + Pro | Pull clips newer than ?since=<cursor> (ISO-8601 timestamp or sequence ID) |
POST |
/sync/push |
Pro only | Push one or more encrypted clips to the vault |
| Method | Route | Description |
|---|---|---|
POST |
/devices/register |
Register the current device and receive a device ID |
GET |
/devices |
List all devices associated with the authenticated account |
DELETE |
/devices/<id> |
Revoke a device by ID |
| Method | Route | Description |
|---|---|---|
GET |
/events/stream |
Open an SSE stream to receive real-time plan change events |
Migrations are managed with Flask-Migrate (Alembic). All migration scripts use render_as_batch=True so that column alterations work correctly on SQLite (which does not support ALTER COLUMN natively).
# Apply all pending migrations
flask db upgrade
# Roll back the most recent migration
flask db downgrade
# Generate a new migration after changing models
flask db migrate -m "describe your change"
# Show current migration state
flask db currentWhen writing new migrations manually, always wrap operations inside with op.batch_alter_table(...) as batch_op: to maintain SQLite compatibility.
# Install dev dependencies if not already present
pip install -r requirements-dev.txt
# Run the full test suite
pytest
# Run with coverage report
pytest --cov=app --cov-report=term-missingTests use an in-memory SQLite database and a dedicated Flask test client — no external services required.
A production compose file with an Nginx reverse proxy is provided:
docker compose -f docker-compose.prod.yml up -d --buildThis stack runs:
- Flask (via Gunicorn) on an internal network
- Nginx as the public-facing reverse proxy (port 443 / 80)
- MySQL as the database backend
- Redis as the rate-limit store
- Set
FLASK_ENV=production - Set a strong, randomly generated
FLASK_SECRET_KEYandJWT_SECRET - Point
DATABASE_URLat your MySQL instance:mysql+pymysql://user:password@host/dbname - Set
REDIS_URLto your Redis instance URL - Optionally set
SENTRY_DSNfor error tracking - Run
flask db upgradeas part of your deployment pipeline before starting the application
It is recommended to run flask db upgrade as a pre-start step (or a Kubernetes init container / ECS task) so migrations complete before traffic is served.
MIT — see LICENSE.