Skip to content

Repository files navigation

FDX

FDX is a multi-tenant event-photo delivery platform. Organizations upload event photographs and participant lists; FDX builds a persistent face index, matches consenting participants from their selfies, and delivers private galleries. A single login serves Super Admins, Organization Admins, and permission-restricted Staff. Participants use expiring enrollment and gallery links without creating an account.

Event photos are analyzed when they are processed. Their detected faces receive event-scoped unique IDs and are stored in PostgreSQL with embeddings and photo associations. A later selfie needs one inference request to create its own embedding; matching then queries the stored event index. Viewing an existing gallery does not run the models.

The product workflow is documented in docs/workflow.md, the technical requirements in docs/specs.md, and the implementation map in docs/spec-implementation.md. See the validation report for checks performed and deployment limits.

What the platform does

  • Manages organizations, administrator invitations, staff permissions, storage quotas, and retention policies.
  • Imports participants from CSV, XLS, XLSX, or XLSM with validation, preview, and idempotent confirmation.
  • Accepts event images through files, folders, ZIP archives, and upload batches; production browser uploads use presigned private S3 requests.
  • Runs RetinaFace detection and AdaFace recognition asynchronously, recording face details and model versions in the database.
  • Applies similarity, face-quality, and runner-up checks before admitting photographs to a participant's gallery.
  • Shows private thumbnails and originals, builds asynchronous ZIP exports, and queues gallery email after processing and approval.
  • Removes event media and derived records on deletion or scheduled retention, while tracking usage and audit events.

Architecture

flowchart TB
    Admin["Administrators and Staff"] --> Web
    Participant["Participants"] --> Web
    Web["React application / NGINX"] --> API["FastAPI API"]
    API --> DB[("PostgreSQL + pgvector")]
    API --> Redis[("Redis: rate limits and health cache")]
    API --> Outbox["Transactional outbox"]
    Outbox --> Kafka["Kafka"]
    Kafka --> Worker["Background worker"]
    DB -->|Durable job fallback| Worker
    Worker --> ML["RetinaFace + AdaFace inference"]
    API -->|New selfie only| ML
    Worker --> DB
    API --> Storage[("Private media storage")]
    Worker --> Storage
    Web -->|Presigned production uploads| Storage
    Worker --> Email["SES / Resend / local outbox"]
Loading

PostgreSQL is the source of truth for users, events, jobs, face identities, matches, and delivery state. Redis is used for rate limiting and health caching. Kafka transports processing work; a persisted PostgreSQL queue lets workers recover queued work when Kafka is unavailable. Original images, selfies, thumbnails, and gallery ZIPs live in private object storage, rather than inside database rows.

Path Responsibility
webapp/ React/Vite dashboards and participant enrollment/gallery pages
backend/app/ FastAPI routes, authorization, SQLAlchemy models, storage/email adapters, matching, and workers
backend/alembic/ Versioned PostgreSQL schema migrations
backend/tests/ Backend security, workflow, storage, and face-index regression tests
face-processing/service/ Gunicorn / ONNX Runtime inference service, CPU/GPU images, and warmup fixture
face-processing/models/ ONNX model locations and integrity manifest
face-processing/service/app.py Inference service implementation
tools/ Model verification, local test runner, and live acceptance checks
deploy/ NGINX configuration, cloud environment contract, and AWS infrastructure/publishing scripts
docs/ Product requirements, implementation map, validation evidence, and cost estimate
compose.local.yml Self-contained local deployment
compose.cloud.yml Application containers connected to managed production dependencies
compose.gpu.yml NVIDIA device reservation for a cloud GPU inference image

The original /api dashboard endpoints and additive /api/v2 endpoints coexist. V2 covers rotating sessions, import preview, upload batches, processing, enrollment, gallery exports, and delivery. Authorization and tenant checks are enforced by the API.

The AWS cost-estimate workbook is the single detailed planning snapshot. It contains illustrative inputs, rather than live pricing. Regenerate that workbook with python tools/create_cost_report.py after reviewing its assumptions; the generator no longer creates additional export copies.

Naming conventions

Use short, descriptive filenames. Python modules and automation scripts in tools/ use snake_case; web helpers and hooks use camelCase; React components use PascalCase; documentation uses kebab-case. Keep standard entrypoints such as README.md, Dockerfile, and app.py. Compose files use compose.<environment>.yml, with compose.gpu.yml as the GPU override. Database migrations keep their revision identifiers.

Model artifacts use retinaface.onnx and adaface.onnx. The checksum manifest identifies the exact model bytes. Model-version settings and stored records retain the architecture and training variant, such as retinaface-r50 and adaface-ir101-ms1mv2, so embedding compatibility remains explicit.

Persistent face identities and matching

sequenceDiagram
    actor Admin as Organization Admin
    participant API as FastAPI
    participant Worker as Background Worker
    participant ML as Face Models
    participant DB as PostgreSQL
    actor Attendee as Participant

    Admin->>API: Upload event photos and complete batch
    API->>DB: Store photo metadata and processing jobs
    Worker->>ML: Detect and embed each unprocessed photo
    ML-->>Worker: Faces, embeddings, landmarks, and quality
    Worker->>DB: Persist detections, unique face IDs, and photo links
    Worker->>DB: Match existing compatible enrollments
    Attendee->>API: Consent and submit a new selfie
    API->>ML: Detect and embed this selfie
    ML-->>API: Participant embedding
    API->>DB: Save enrollment and compare with stored event face index
    DB-->>API: Persisted eligible photo matches
    API-->>Attendee: Private matched-photo gallery
    Attendee->>API: Reopen gallery
    API->>DB: Read saved matches and authorize media
    API-->>Attendee: Gallery without inference
Loading
Record Stored information
photos Event/tenant ownership, object keys, checksums, sizes, and processing status
face_detections Detection UUID, photo/unique-face references, bounding box, landmarks, face dimensions, quality class, confidence, embedding, and detector/embedder versions
unique_faces Event-scoped identity UUID, centroid/vector and accumulation state, occurrence count, model version, and timestamps
unique_face_photos Unique face-to-photo mapping and detection count, with duplicate mappings prevented
face_enrollments Participant selfie object reference, embedding/vector, quality, model version, validity, and expiry
face_matches Per-detection participant assignment, similarity, runner-up score, margin, decision state, and threshold/model provenance
erDiagram
    ORGANIZATIONS ||--o{ EVENTS : own
    EVENTS ||--o{ PHOTOS : contain
    EVENTS ||--o{ UNIQUE_FACES : index
    EVENTS ||--o{ PARTICIPANTS : include
    PARTICIPANTS ||--o| FACE_ENROLLMENTS : submit
    PHOTOS ||--o{ FACE_DETECTIONS : produce
    UNIQUE_FACES o|--o{ FACE_DETECTIONS : group
    UNIQUE_FACES ||--o{ UNIQUE_FACE_PHOTOS : link
    PHOTOS ||--o{ UNIQUE_FACE_PHOTOS : appear_in
    FACE_DETECTIONS ||--o| FACE_MATCHES : receive
    PARTICIPANTS o|--o{ FACE_MATCHES : identify
Loading

A unique face represents an inferred cluster within one event and embedding-model version. It is not a verified civil identity or an identifier shared across organizations. Repeated appearances can share that UUID; rejected detections may have no cluster. Clustering is an aid to retrieval, and gallery eligibility still depends on each detection's matching policy.

Successful processing results are reused. Explicit retries or reprocessing can run inference again, and rebuilding an index may change cluster membership or IDs. New uploads extend the event index. Selfie replacement updates enrollment and affected database matches. Incompatible model versions and expired enrollments are excluded; a model upgrade requires deliberate reprocessing and re-enrollment instead of comparing incompatible vectors.

Defaults use a cosine similarity cutoff of 0.86, a runner-up margin of 0.10, and an additional 0.05 cutoff for low-resolution faces. UNIQUE_FACE_CLUSTER_THRESHOLD=0.75 governs clustering separately. These scores are not identity probabilities; calibrate them with representative event data before launch.

Local setup

Install Docker Engine with the Compose plugin, Bash, OpenSSL, and standard GNU shell utilities. The local stack runs PostgreSQL/pgvector, Redis, Kafka, API, worker, inference, and NGINX. Node.js 22 and Python 3.12 are needed for checks outside containers.

Supply the two ONNX artifacts at these exact paths:

face-processing/models/detection/retinaface.onnx
face-processing/models/recognition/adaface.onnx

For an existing checkout, rename retinaface-r50.onnx to retinaface.onnx in the detection directory and adaface-ir101-ms1mv2.onnx to adaface.onnx in the recognition directory. This changes filenames only; keep the same model bytes and model-version settings.

The model files are not downloaded automatically. They must match the repository manifest and be readable by inference container UID 10001.

./tools/verify_models.sh
./start.sh

The first start creates a private .env with randomly generated database, JWT, administrator, webhook, and verification credentials. Open http://127.0.0.1:8080 and sign in with FDX_SUPER_ADMIN_EMAIL and FDX_SUPER_ADMIN_PASSWORD from that file. Keep .env private. If an existing .env predates a newly required setting, add it from .env.example; the launcher does not overwrite existing credentials.

Only the web port is published, bound to loopback. API, inference, database, Redis, and Kafka remain on the Docker network. Local email is recorded in the application's outbox; the default does not send external messages.

docker compose --env-file .env -f compose.local.yml ps
docker compose --env-file .env -f compose.local.yml logs --tail=100 api worker ml
./stop.sh

Stopping preserves Docker volumes. Keep those volumes when upgrading or restarting. Deleting volumes also deletes local database and media storage.

Event workflow

  1. A Super Admin creates an organization and invites an Organization Admin.
  2. The Organization Admin creates an event, imports participants, and sends enrollment invitations.
  3. Event photographs are uploaded and processed. Participants can enroll before or after photo processing.
  4. A participant consents and submits a clear selfie; FDX matches it against the persisted event face index and displays eligible photos.
  5. The dashboard shows uploaded/processed counts, failures, matching results, and email approval state.
  6. Once all uploads and photos are complete, the Organization Admin approves gallery email, or the event's enabled automatic-send policy queues it.
  7. Participants revisit their private links or download individual images and asynchronous ZIP exports until the links or event expire.

Events default to manual email approval. Starting another upload or processing attempt clears approval and pauses queued gallery email. Failed or cancelled photos require a successful retry before delivery. Repeated approval does not enqueue duplicate messages; sending a previously sent gallery again requires explicit resend. Enrollment and team invitations can be sent before processing finishes.

Configuration

Use .env.example for local deployment and deploy/cloud.env.example for the cloud contract. Compose validates required settings before startup. Values below describe the supplied Compose configuration.

Settings Purpose / default
POSTGRES_PASSWORD, JWT_SECRET, FDX_SUPER_ADMIN_PASSWORD, EMAIL_WEBHOOK_SECRET Required secrets; generated locally, supplied from a secret manager in production
FDX_WEB_PORT Local loopback web port; 8080
DATABASE_URL, REDIS_URL, KAFKA_BOOTSTRAP_SERVERS Required managed dependency addresses in cloud deployments
API_IMAGE, WEB_IMAGE, ML_IMAGE Required cloud image references; prefer immutable release digests
FRONTEND_URL, S3_BUCKET, AWS_REGION, EMAIL_FROM Public HTTPS origin, private media bucket, region, and verified production sender
TRUSTED_PROXY_CIDRS Cloud NGINX ingress proxies allowed to supply client IP/protocol; empty by default
FORWARDED_ALLOW_IPS Uvicorn forwarding trust; Compose uses * within its private API network
ACCESS_TOKEN_MINUTES, REFRESH_TOKEN_DAYS Access token lifetime 15 minutes; rotating refresh session lifetime 7 days
ENROLLMENT_TOKEN_DAYS, GALLERY_TOKEN_DAYS Enrollment/gallery link lifetimes; both 7 days in Compose
MAX_UPLOAD_BYTES Aggregate upload allowance; 100 GiB
MAX_MEDIA_FILE_BYTES, MAX_ENROLLMENT_BYTES Individual media 100 MiB; selfie 15 MiB
MAX_IMAGE_PIXELS Decode limit; 100,000,000 pixels
DB_POOL_SIZE, DB_MAX_OVERFLOW Per-process connection budget; 5 + 5
WORKER_CONCURRENCY, ML_INFERENCE_THREADS Photo jobs per worker and inference threads; both 4
MATCH_AUTO_THRESHOLD, MATCH_RUNNER_UP_MARGIN Gallery matching policy; 0.86, 0.10
UNIQUE_FACE_CLUSTER_THRESHOLD Similarity required to group compatible appearances; 0.75
FDX_DETECTOR_MODEL_VERSION, FDX_EMBEDDER_MODEL_VERSION, THRESHOLD_PROFILE_VERSION Persisted provenance and embedding compatibility
CONSENT_POLICY_VERSION Version attached to recorded participant consent

The NGINX request body limit is 10 GiB; individual media and application quota checks also apply. Browser S3 uploads bypass that proxy body limit. Size budgets are limits rather than capacity promises; tune concurrency, temporary storage, and database connections against measured workloads.

Formatting, linting, tests, and builds

From the repository root:

python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install -r backend/requirements-dev.txt
npm --prefix webapp ci
make format
make format-check
make lint
make test
make build

Install ShellCheck for shell linting and jq for deployment tests. make format applies Prettier to supported JavaScript/JSX, CSS, JSON, Markdown, YAML, and HTML files and Ruff to Python; binary assets and generated directories are excluded. Dockerfiles and shell scripts are checked using their applicable tools rather than passed to Prettier.

make test runs frontend tests, backend tests against an isolated Docker PostgreSQL/pgvector database, and offline deployment-publication regressions. make check combines the routine formatting, lint, test, and frontend-build checks. make audit checks npm and Python dependencies. CI also validates Compose, lints CloudFormation, checks shell syntax, runs migrations, and builds application container images.

With model artifacts present, run the optional real-model Docker smoke checks:

make test-ml

This verifies model checksums, builds the CPU inference image, and checks readiness, normalized 512-dimensional embeddings, and input validation.

make test-proxy runs Docker integration checks for trusted proxy client addresses, HTTPS forwarding, and rejection of spoofed forwarding headers.

For integration checks against a running disposable local/staging deployment:

node tools/verify_platform.mjs
FDX_VERIFY_FACE_IMAGE=/path/to/clear-face.jpg node tools/verify_platform.mjs
FDX_VERIFY_FACE_IMAGE=/path/to/clear-face.jpg node tools/verify_workflow.mjs

The live verifiers create and mutate test records, exercise tenant/staff authorization, uploads, enrollment, processing, galleries, delivery, and deletion. They read credentials from the private .env. V2 requires a real face image. FDX_VERIFY_XLS=/path/to/participants.xls adds legacy Excel coverage to the platform verifier. Unit tests do not substitute for this real-model integration path.

Database migrations and upgrades

The API entrypoint runs alembic upgrade head before serving traffic. Migration and bootstrap advisory locks serialize competing API starts. Workers set RUN_MIGRATIONS=false and wait for API readiness. Schema history lives in backend/alembic/versions/; tables are not implicitly created in production.

Inspect a running local database's migration state:

docker compose --env-file .env -f compose.local.yml exec api alembic current

Before upgrading, back up database and media, review pending migrations, and run the new build and acceptance flow in staging. Face-index migration 20260911_12 records detection embedding-model versions and accurate centroid accumulation; existing cluster UUIDs remain in place. Older rows with unresolvable provenance cannot safely be assumed compatible with a newly installed model.

Use additive migrations for releases that run old and new containers concurrently. Rolling the image back does not roll the database schema back. Restore a tested backup or apply a reviewed corrective migration if data compatibility prevents an image-only rollback.

Cloud deployment and GPU inference

deploy/README.md describes the runtime contract; deploy/aws/README.md covers the AWS deployment. The template provisions a two-AZ VPC, HTTPS ALB, EC2 Auto Scaling, RDS PostgreSQL, ElastiCache Redis, MSK Kafka, ECR, private/versioned S3, SES identity, IAM, Secrets Manager, SSM, and scheduled retention.

For another deployment environment, populate a private copy of deploy/cloud.env.example, mount readable model artifacts at /opt/fdx/face-processing/models, then start:

docker compose --env-file /secure/path/fdx-production.env -f compose.cloud.yml config --quiet
docker compose --env-file /secure/path/fdx-production.env -f compose.cloud.yml up -d

Use a private network with HTTPS ingress to cloud port 8080. Production Compose requires managed services, private S3 storage, SES, and production secrets. It does not provision them. Container logs rotate locally, but durable centralized log collection and alerts still need configuration.

NGINX ignores incoming client IP/protocol headers unless the connection originates from an explicitly configured TRUSTED_PROXY_CIDRS address. The AWS bootstrap trusts its ALB subnets, and the ALB appends the real client address. NGINX discards the incoming forwarding chain and supplies one resolved address to the API, so rate limits and audit records use the client instead of a shared proxy. See the proxy trust contract before changing ingress or publishing API ports.

To use an NVIDIA host with compatible drivers and NVIDIA Container Toolkit, build and publish the separate GPU image, set ML_IMAGE to that image, and include the GPU overlay:

docker build -f face-processing/service/Dockerfile.gpu -t fdx-ml:gpu .
docker compose --env-file /secure/path/fdx-production.env \
  -f compose.cloud.yml -f compose.gpu.yml up -d

The overlay reserves one NVIDIA GPU and selects CUDA. The regular image contains CPU ONNX Runtime. The supplied AWS launcher uses CPU hosts/images by default; its publisher does not provision a GPU host or automatically enable this overlay.

Operations and release validation

Signal Meaning
/health/live API process is responding; reachable within the application network
/health/ready API can query its database; also routed through NGINX for the load balancer
/health/dependencies Dependency diagnostics; available directly on the API
/metrics In-process API request/error/latency counters in Prometheus text format
Worker jobs and logs Processing progress, retries, failure states, and queue recovery
Email outbox and deliveries Provider acceptance, retries, and gallery approval state

Readiness alone does not prove ML, Kafka, email, or storage functionality. Watch queue age, repeated retries, database connections, host memory/disk, storage growth, and inference health in addition to HTTP probes. API metrics reset on process restart and must be collected per replica.

Create a local database backup without displaying credentials:

mkdir -p backups
chmod 700 backups
umask 077
docker compose --env-file .env -f compose.local.yml exec -T postgres \
  pg_dump -U fdx -d fdx -Fc > backups/fdx.dump

Also back up the object_storage volume with a consistent snapshot while uploads, processing, and deletion are paused. Database backups alone do not contain the images. For AWS, the template retains RDS backups for 14 days and enables S3 versioning; regularly test restoring both metadata and corresponding object versions in an isolated environment.

Application deletion removes active object keys and derived database records. Versioned S3 storage can retain older object versions until its configured 30-day noncurrent-version expiry; backups have their own retention. Align those settings with the declared deletion policy. The independent S3 lifecycle expires event objects after DeleteAfterDays (default 365), so event retention must not promise availability beyond that ceiling.

Before promoting a release, run the real-model acceptance flow with private S3, managed dependencies, HTTPS, and outbound email; test backup restoration; calibrate matching against the intended event population; and measure target event sizes and concurrent uploads. The repository supplies production deployment components and automated checks, but source checks cannot establish the availability, model accuracy, delivery success, or capacity of an untested live installation.

About

FDX is a multi-tenant event-photo delivery platform

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages