Prototype for Firefox content sharing — lets users create and share collections of links. Built with Django 6, Python 3.14, managed with uv.
Requires Docker Desktop. Everything else (Python, Postgres, Redis) runs inside Docker.
make setup # generate .env with a random SECRET_KEY
make up # build and start app, Postgres, Redis, Celery worker, and FlowerThe app will be available at http://localhost:8000. Migrations run automatically on make up.
To monitor Celery tasks, open Flower — a real-time dashboard showing worker status, task history, and failure tracebacks.
To tail worker logs directly:
docker compose logs -f workerRequires Python 3.14+, uv, a running PostgreSQL instance, and a running Redis instance.
make setup # install dependencies and generate .envSet DATABASE_URL and REDIS_URL in .env to your local connection strings, e.g.:
DATABASE_URL=postgres://localhost/fxsharing
REDIS_URL=redis://localhost:6379/0
Then run each of these in separate terminals:
make migrate # apply migrations (first time only)
make run # start the dev server
make worker # start the Celery worker
make flower # start the Flower task monitor (http://localhost:5555)Favicons are uploaded to a Google Cloud Storage bucket. make setup copies
.env.example to .env, which already points local dev at a project made specifically for testing:
GCS_IMAGE_BUCKET=favicon-bucket-2
GOOGLE_CLOUD_PROJECT=niklas-test-fx-sharing
GOOGLE_APPLICATION_CREDENTIALS=/app/.gcloud_credentials
(In dev/prod these are injected via k8s and auth uses Workload Identity, so you don't set them there.)
To get credentials for the test project locally:
-
Install the Google Cloud SDK: https://docs.cloud.google.com/sdk/docs/install-sdk
-
Ask a project admin to grant your Google account access to
niklas-test-fx-sharing(at least theStorage Object Adminrole onfavicon-bucket-2). -
Generate Application Default Credentials:
gcloud auth application-default login
-
The app runs in Docker, where the project root is bind-mounted to
/app. Copy the credentials into the repo root as.gcloud_credentialsso the container can read them at/app/.gcloud_credentials(the path.envalready expects):cp ~/.config/gcloud/application_default_credentials.json .gcloud_credentials.gcloud_credentialsis gitignored, so it won't be committed. Re-run thiscpwhenever you refresh your credentials withgcloud auth application-default login.
If GCS_IMAGE_BUCKET is unset, favicon uploads are skipped — the app still works without bucket
storage configured.
The /__lbheartbeat__, /__heartbeat__, and /__version__ endpoints are provided by the python-dockerflow library.
GET /__lbheartbeat__— load balancer health checkGET /__heartbeat__— application health checkGET /__version__— deployed version infoPOST /api/v1/create— create a share (requires authentication; JSON body, seeshare_schema.pyfor schema)GET /<shortcode>— view share pagePOST /report/<shortcode>— report a share (form POST,reasonfield required; valid values:copyright,harmful,spam,other)POST /api/v1/ts_response— Cinderdecision.createdwebhook receiver (HMAC-signed viaCINDER_WEBHOOK_TOKEN, seecinder_schema.pyfor the expected payload shape)
To test authenticated endpoints locally, log in first via the dummy FxA provider at http://localhost:8000/accounts/dummy/login/.
Populate the database with diverse, edge-case sample data (shares with the maximum number of links, small shares, nested shares, expired shares, soft-deleted shares, a banned user, a soft-deleted user, and shares in various moderation statuses):
make seedmake seed targets the right database automatically: if the Docker stack is
running (make up), it seeds inside the app container; otherwise it seeds your
local database. This matters because the two run against different databases —
seeding the host while the Dockerized app reads the container's database would
leave the app looking empty.
The command is idempotent — every run wipes the previously seeded users (their
fxa_id is prefixed with seed-) and recreates everything from scratch. It
only runs when DEBUG=True; with DEBUG=False it exits with an error.
When DEBUG=True, a dev-only login page is available at
http://localhost:8000/dev-login. It lists
every user (including banned and soft-deleted ones) and lets you log in as any of
them with one click — no real FxA OAuth required — so you can manually QA
authenticated flows as a specific seed user. The same page has a log-out button.
This route does not exist when DEBUG=False.
The app's content-safety integration POSTs each shared URL to
Cinder's link_sharing_quality workflow and listens
for the resulting decision.created webhook on /api/v1/ts_response. For
local development, scripts/mock_cinder.py stands in for the real Cinder
service: it accepts the workflow event POSTs and fires signed decision.created
webhook callbacks back at the app.
Start the mock in its own terminal:
make mock-cinderThe mock listens on http://localhost:8081. Point the app at it by setting
the following in .env and restarting the dev server (or the app container):
CINDER_URL=http://localhost:8081
CINDER_WEBHOOK_TOKEN=any-string-you-like
The mock signs its callbacks using CINDER_WEBHOOK_TOKEN, so the same value
must be in the environment where the mock runs (it inherits from .env via
uv run). If the secrets don't match, ts_webhook rejects the callback as
an invalid signature.
The mock decides which Cinder branch to simulate from the submitted URL:
- contains
malware,phishing, orunwanted→ Web Risk threat → share is markedBLOCKED(whole lineage). - contains
csamorncmec→ NCMEC hash match → share is markedBLOCKED. - anything else → approve, share stays
ACTIVE.
http://malware.testing.google.test/testing/malware/ is Google's canonical
Web Risk test URL and is convenient for exercising the high-risk path.
Useful flags:
--delay <seconds>— wait this long before firing the webhook (default0.5, simulates Cinder latency). Pass0to fire beforecreate_shareeven responds to the browser.--webhook-url <url>— override the receiver URL when the app isn't onhttp://127.0.0.1:8000.
Both directions are JSON-Schema validated against
fxsharing/shares/cinder_schema.py: the mock rejects malformed workflow events
with 400, and ts_webhook rejects malformed decision.created payloads the
same way. The mock implements only the standard signed decision.created
webhook; Cinder's optional unsigned observability webhook isn't simulated.
make testTests use pytest with pytest-django. CI runs tests automatically on all pull requests.