Skip to content

External Services

This page documents every third-party service used by the platform: its purpose, the environment variables it requires, and operational considerations.


Stripe

Purpose: Payment processing for photo package purchases.

Used by: main-api

Keys

VariableDescription
STRIPE_SECRET_KEYServer-side secret key (sk_live_... or sk_test_...)
STRIPE_WEBHOOK_SECRETSigning secret for webhook event verification (whsec_...)

How it works

  1. A user selects a package — POST /v1/purchases creates a Stripe PaymentIntent and returns its client_secret.
  2. The browser completes payment with Stripe.js.
  3. Stripe sends a payment_intent.succeeded webhook to main-api.
  4. The webhook handler (verified with STRIPE_WEBHOOK_SECRET) sets purchase.status = 'paid' and enqueues notify_purchase_confirmed.

Considerations

  • Use sk_test_... + Stripe CLI webhook forwarding in development (stripe listen --forward-to localhost:3002/v1/stripe/webhook).
  • The webhook endpoint must be reachable by Stripe (use ngrok or similar in local dev).
  • STRIPE_WEBHOOK_SECRET differs between test and production environments — regenerate it in the Stripe dashboard for each environment.
  • Never log the full PaymentIntent or card details.

Resend

Purpose: Transactional email (invites, purchase confirmations, password resets).

Used by: main-api

Keys

VariableDescription
RESEND_API_KEYResend API key (re_...)

Emails sent

JobTrigger
notify_user_invitedOperator sends bulk invites via backoffice
notify_purchase_confirmedStripe webhook confirms a payment
notify_password_reset_requestedUser requests a password reset
notify_user_taggedA face tag is confirmed in a photo

Considerations

  • All emails are sent asynchronously via BullMQ workers — the request that triggers them returns immediately.
  • The notify_user_invited worker uses MAIN_WEB_URL (media-api env) to build invite links.
  • In development, point RESEND_API_KEY at a test API key and use a Resend sandbox domain to avoid sending real emails.

Cloudflare R2

Purpose: Object storage for all media files: original photos, thumbnails, watermarked variants, and user selfies.

Used by: media-api

Keys

VariableDescription
R2_ACCOUNT_IDCloudflare account ID
R2_ACCESS_KEY_IDR2 API token access key
R2_SECRET_ACCESS_KEYR2 API token secret key
R2_BUCKET_NAMER2 bucket name
R2_PUBLIC_URLBucket endpoint URL (used to construct presigned URLs)

Key naming conventions

selfies/{userId}/{timestamp}.jpg
photos/{projectId}/{photoId}/original.jpg
photos/{projectId}/{photoId}/thumb-400.jpg
photos/{projectId}/{photoId}/thumb-800.jpg
photos/{projectId}/{photoId}/watermarked.jpg

Considerations

  • Never return a public R2 URL to the client. Always issue presigned GET URLs with a 15-minute expiry.
  • All uploads use presigned PUT URLs — the server never proxies file bytes.
  • EXIF data (including GPS) is stripped by Sharp before writing thumbnails and watermarked variants. The original retains EXIF only on R2, never exposed to the client.
  • On project deletion, R2 objects are deleted in batches by the cleanup_project_storage worker (best-effort; orphaned files are non-harmful).
  • R2 is S3-compatible — use any S3 SDK. The project uses @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner.

Google Cloud Run — Retina (Face Clustering)

Purpose: Stateless face embedding and clustering service. Replaces AWS Rekognition.

Used by: media-api (workers)

Keys

VariableDescription
RETINA_SERVICE_URLFull URL to the Cloud Run service (e.g. https://retina-xxx-ew.a.run.app)
RETINA_API_KEYSent as x-api-key header on every request

How it works

  • process_selfie worker calls POST /analyze with the selfie image. Retina returns a 512-dim embedding and a face detection score (0–1). If the score is ≥ 0.9, the selfie is approved and the embedding stored; otherwise it is rejected with a rejectionReason.
  • cluster_faces worker sends the user's selfie and up to 50 project photo thumbnails per request to POST /cluster. Retina returns per-face cluster IDs. Photos whose cluster ID matches the selfie are tagged as confirmed.

Considerations

  • Workers are idempotent — running the same job twice produces the same DB state.
  • Retina calls are never made synchronously inside a request handler. Always via BullMQ workers.
  • The 512-dim embeddings are stored as vector(512) using pgvector — never as JSON or text.
  • If RETINA_SERVICE_URL is unset, workers skip Retina calls gracefully; selfies remain in pending state.

Google Maps Platform

Purpose: Location autocomplete and timezone detection in the backoffice project create/edit forms.

Used by: backoffice-web (client-side, browser only)

Keys

VariableDescription
NEXT_PUBLIC_GOOGLE_PLACES_API_KEYGoogle Maps Platform API key — exposed to the browser. Must be set at build time.

APIs enabled (all under the same key)

  • Maps JavaScript API — required by @googlemaps/js-api-loader to bootstrap the SDK.
  • Places API (New)AutocompleteSuggestion.fetchAutocompleteSuggestions() for programmatic location search; PlacePrediction.toPlace().fetchFields(['location']) to resolve coordinates.
  • Time Zone API — REST call to maps.googleapis.com/maps/api/timezone/json to convert coordinates to an IANA timezone string.

How it works

  1. As the operator types in the Location field, the component queries Places API New and shows a custom dropdown (no Google widget).
  2. On selection, toPlace().fetchFields(['location']) resolves lat/lng within the same billing session.
  3. The lat/lng is passed to the Time Zone API to auto-populate the timezone selector.

Billing session tokens The component uses AutocompleteSessionToken to group autocomplete + fetchFields calls into a single billable session. newToken() is called after fetchFields completes to start a fresh session for the next search.

Setup

  1. Create or select a project in Google Cloud Console.
  2. Enable: Maps JavaScript API, Places API (New), Time Zone API.
  3. Create an API key. In production, restrict it to your backoffice domain (HTTP referrer restriction) and the three APIs above.
  4. Set the key as NEXT_PUBLIC_GOOGLE_PLACES_API_KEY in apps/backoffice-web/.env.local (dev) or your deployment environment (prod).

Considerations

  • The key is exposed in the browser bundle — restrict it by HTTP referrer in production to prevent misuse.
  • All three APIs must be enabled on the same key; enabling only Places API causes an ApiTargetBlockedMapError.
  • Timezone detection is best-effort: if the Time Zone API call fails, the operator can still select a timezone manually.
  • In development without a key, the location input falls back to a plain text field (autocomplete silently skipped).

Wairo — Internal Documentation