Firewatch Cloud deployment contract
Purpose
Section titled “Purpose”The public firewatch repository builds the complete portable product. The
private firewatch-cloud repository deploys and operates it, and must consume
the contract below instead of copying product source or introducing a separate
Cloud/Enterprise implementation.
This document is the interface between those repositories. Values shown here are configuration names and examples, not cloud resources. The public repository does not contain AWS Terraform, managed-cloud deployment code, paid-plan logic, or customer-specific secrets.
Ownership boundary
Section titled “Ownership boundary”The private firewatch-cloud repository owns:
- AWS infrastructure-as-code and production networking;
- multi-region failover;
- managed PostgreSQL and queue provisioning;
- billing and subscription systems;
- customer provisioning and internal administration;
- managed SMS, voice, email, and push-provider configuration;
- disaster-recovery procedures and exercises;
- cloud deployment pipelines;
- private operational dashboards.
- paid authentication adapters such as enterprise SAML and their customer connection configuration.
The public repository owns all portable product behavior, public database migrations, the REST and message contracts, API-client generation, and the API, worker, and web image definitions. A private extension may own migrations only for its own namespaced schema and must not alter public tables or migration history. A change needed by both deployment models belongs in public product code.
Versioned artifacts
Section titled “Versioned artifacts”A release produces three OCI images from the public repository:
| Image | Process | Internal port | Persistent filesystem |
|---|---|---|---|
firewatch/api:<version> |
ASP.NET Core REST API | TCP 8080 |
None |
firewatch/worker:<version> |
.NET background worker | None | None |
firewatch/web:<version> |
Static React assets and Nginx proxy | TCP 8080 |
None |
The eventual registry hostname is intentionally not promised yet. Release
automation should publish a SemVer tag, an immutable source-revision tag, and an
OCI manifest digest. Cloud deployments must pin a digest, record the product
version, and promote the same digest between environments. latest is not a
deployment contract.
API and worker images from a release must be deployed at compatible versions. The web image should normally match them. Rollbacks remain constrained by the database migration notes below.
Cloud may build additive API and web overlays for private extensions. The API overlay
must pin the public firewatch/api base image by digest, copy extension artifacts
under /app/extensions, and select each assembly explicitly through
Firewatch__Extensions__Assemblies__N. It must not replace public binaries,
copy public source into a fork, or own migrations for public tables. A private
web build may fill the public product-extension registry while compiling from a
pinned public checkout. The public API remains responsible for account linking,
organization authorization, and session creation after an extension validates
an external identity.
Runtime configuration
Section titled “Runtime configuration”.NET configuration uses the standard double-underscore environment mapping. Secrets must come from the deployment platform’s secret store, never from an image or source-controlled environment file.
API and worker
Section titled “API and worker”| Variable | Required | Meaning |
|---|---|---|
ConnectionStrings__Firewatch |
Yes | Npgsql connection string for the shared Firewatch PostgreSQL database |
Queueing__Provider |
Worker only | Postgres for self-hosting or Sqs for the managed worker transport |
Database__MigrateOnStartup |
API only | Local convenience only; keep false in managed/multi-replica environments |
Auth__RegistrationMode |
API only | Disabled, Bootstrap, Open, or Provisioned; managed Cloud must use Provisioned |
Auth__PublicBaseUrl |
API only | Canonical external HTTPS origin used to construct invitation and account-recovery links |
Auth__RequireEmailVerification |
API only | Managed Cloud must use true; self-hosted bootstrap defaults to false |
SelfHosting__Enabled |
API only | Enables the self-hosted setup guide; managed Cloud must use false |
Auth__Passkeys__ServerDomain |
API only | Optional WebAuthn RP ID override; otherwise inferred from the canonical public URL |
Auth__DataProtection__CertificatePath |
Managed Cloud | Path to a mounted PKCS#12 certificate used to encrypt the shared Data Protection key ring |
Auth__DataProtection__CertificatePassword |
Managed Cloud secret | Password for the mounted Data Protection certificate |
Auth__DataProtection__PreviousCertificates__N__Path |
Certificate rotation | Mounted PKCS#12 certificate previously used to encrypt keys (N starts at 0) |
Auth__DataProtection__PreviousCertificates__N__Password |
Rotation secret | Password for the matching previous certificate |
Auth__Provisioning__Enabled |
Cloud provisioning only | Must be true to expose paid-customer provisioning; defaults to false |
Auth__Provisioning__Authority |
Cloud provisioning only | OIDC issuer trusted to mint workload access tokens |
Auth__Provisioning__Audience |
Cloud provisioning only | Audience that uniquely identifies the Firewatch provisioning API |
Auth__Provisioning__RequiredScope |
Cloud provisioning only | Required workload-token scope; must remain firewatch.provision |
Features__UserProfileEditing |
API only | Self-hosted defaults to true; managed Cloud should set false |
Firewatch__Extensions__Assemblies__N |
Cloud API overlay | Absolute path to a trusted private extension assembly; N starts at 0 |
Email__Enabled |
API and worker | Enables account and incident email; keep disabled until the selected provider is valid |
Email__Provider |
API and worker | Smtp for portable/self-hosted delivery or Ses for the managed AWS adapter |
Email__FromAddress |
API and worker | Verified sender address; defaults to noreply@onfirewatch.com |
Email__FromName |
API and worker | Sender display name; defaults to Firewatch |
Email__Smtp__Host |
API and worker | SMTP server hostname |
Email__Smtp__Port |
API and worker | SMTP port, normally 587 for STARTTLS or 465 for implicit TLS |
Email__Smtp__Security |
API and worker | None, StartTls, or SslOnConnect; production must use an encrypted mode |
Email__Smtp__Username |
API and worker secret | Optional SMTP username; configure together with a password |
Email__Smtp__Password |
API and worker secret | Optional SMTP password; source from the deployment secret store |
Email__Ses__ConfigurationSetName |
API and worker | Optional SESv2 configuration set; Cloud supplies its transactional set |
Runbooks__Provider |
API only | FileSystem for Compose or S3 for managed object storage |
Runbooks__StoragePath |
API only | Persistent directory required by the FileSystem provider |
Runbooks__S3__BucketName |
API only | Private S3 bucket required by the S3 provider |
Runbooks__S3__KeyPrefix |
API only | Object prefix; defaults to runbooks |
Twilio__Enabled |
API and worker | Enables phone verification and configured incident phone channels |
Twilio__AccountSid |
API and worker secret | Managed Twilio account SID |
Twilio__AuthToken |
API and worker secret | Managed Twilio auth token |
Twilio__FromPhoneNumber |
API and worker | SMS/voice-capable E.164 sender |
Twilio__IncidentSmsEnabled |
Worker only | Sends incident SMS to verified primary phone numbers; defaults to true |
Twilio__IncidentVoiceEnabled |
Worker only | Enables automated incident calls; defaults to false |
Slack__Enabled |
API and worker | Enables workspace installation, interactive alerts, and lifecycle synchronization |
Slack__ClientId |
API and worker secret | Hosted Slack app client ID |
Slack__ClientSecret |
API and worker secret | Hosted Slack app client secret |
Slack__SigningSecret |
API and worker secret | Verifies timestamped Slack interaction signatures |
Slack__Scopes |
API and worker | Bot scopes; defaults to chat:write,im:write,users:read,users:read.email |
Logging__LogLevel__Default |
No | Baseline structured-log level; defaults to Information |
OTEL_SERVICE_NAME |
Recommended | Stable service identity such as firewatch-api or firewatch-worker |
ASPNETCORE_HTTP_PORTS |
API only | API listen port; image default is 8080 |
Managed Firewatch Cloud uses Email__Provider=Ses and the SESv2 API with
noreply@onfirewatch.com under a verified domain identity. The AWS SDK obtains
credentials from the compute workload role. The SMTP adapter remains the
self-hosted default so operators can supply any standards-compliant mail server.
The product appsettings default is Auth__RegistrationMode=Disabled, while the
self-hosted Compose stack uses Bootstrap. Bootstrap permits the first
administrator registration and then closes automatically. Open deliberately
exposes ongoing public account registration. Managed Firewatch Cloud sets the
mode to Provisioned: public registration remains unavailable, while invitation
acceptance and the authenticated provisioning workflow remain available. The web
application discovers this capability from the API; the cloud deployment must
not build a different web image just to hide registration.
Deployment-specific product behavior is exposed to the web application through
fine-grained session capabilities and is enforced again by the API. The web app
must not infer behavior from an environment name or a generic Cloud flag.
Features__UserProfileEditing=false lets managed deployments keep names under
central identity administration. Future deployment-specific behavior should use
the same capability pattern rather than branching directly on hosting mode.
Auth__PublicBaseUrl must be the single canonical public origin, for example
https://app.firewatch.example, without an internal service name or path. The
API must not derive security-sensitive links from an untrusted Host or
forwarded header. It is also the default WebAuthn origin and source for the
passkey RP ID. Cloud ingress must redirect HTTP to HTTPS and forward only headers
assigned by trusted proxies. An RP ID override is a long-lived security boundary:
changing it invalidates the use of passkeys registered under the previous RP ID.
Cloud customer provisioning
Section titled “Cloud customer provisioning”The private cloud control plane starts provisioning only after the corresponding
payment or subscription transition is durable. It obtains a short-lived OIDC
client-credentials/workload token whose audience matches
Auth__Provisioning__Audience and whose scope includes
firewatch.provision. The product provisioning endpoint must also be restricted
to private ingress. Network isolation supplements token validation; it does not
replace it. Customer organization API keys, user access tokens, and a shared
static bootstrap secret are not valid provisioning credentials.
Each request carries a stable provisioningId idempotency key plus a distinct
externalCustomerId, organization name, and initial administrator’s
first name, last name, and normalized email address. In one database transaction,
the public product creates or retrieves the organization, creates the pending
user when needed, grants the organization-administrator membership, and issues a
single-use, expiring invitation. A durable provisioning-operation row serializes
requests with the same key and returns the same identifiers and invitation token
when an identical request is retried; GET /api/v1/provisioning/operations/{id}
replays that result. Reusing a key with different input is a conflict, not a
second customer. The private control plane retains the key and result so a
payment event, timeout, or process restart can be retried safely.
Provisioning never supplies or receives a plaintext password. Invitation tokens
are high-entropy; their validation value is stored only as a hash. The stable
provisioning replay value is separately encrypted by the Data Protection key
ring, expires, and becomes unusable after its first successful redemption. The
managed email provider delivers the token in the URL fragment, which is not sent
in the HTTP request, and the SPA removes it from browser history immediately.
The token must not be placed in application logs. Accepting an
invitation is not public self-registration and remains available in
Provisioned mode.
The authority and audience are ordinary configuration, but the OIDC client credential is a cloud-control-plane secret and is never injected into the Firewatch API. Issuer signing-key rotation must be supported through OIDC discovery. Changing the issuer or audience is a deployment-contract change and must be coordinated with the control plane.
OpenTelemetry instrumentation hooks are registered, but this skeleton does not
bundle an exporter. Consequently OTEL_EXPORTER_OTLP_ENDPOINT is not an active
runtime setting yet. A future public-core release must add and document an
exporter before the private deployment repository relies on that variable.
PostgreSQL-provider settings require no AWS credentials. The provider uses the same database and connection string as durable application state.
The optional PostgreSQL tuning keys are
Queueing__Postgres__BatchSize (default 20),
Queueing__Postgres__MaximumAttempts (default 10),
Queueing__Postgres__VisibilityTimeout (default 00:02:00), and
Queueing__Postgres__BaseRetryDelay (default 00:00:05). Change them only with
queue-latency, handler-runtime, and database-capacity evidence; they do not alter
at-least-once semantics.
For SQS, the adapter configuration path is Queueing__Sqs. A managed deployment
supplies at least Queueing__Sqs__QueueUrl and an AWS region through the normal
AWS SDK configuration (AWS_REGION or workload metadata). Credentials should
use the workload identity/role credential chain; a non-AWS compute platform may
use a dedicated, least-privilege runtime key until the worker moves to AWS.
MaxNumberOfMessages, WaitTimeSeconds, VisibilityTimeoutSeconds,
VisibilityHeartbeatSeconds, MaximumReceiveCount, and
RetryBaseDelaySeconds are configurable. Runtime receive/visibility settings
must match the queue and dead-letter policy provisioned by the cloud stack.
Queueing__Sqs__MessageGroupId defaults to firewatch and is used only for a
FIFO queue URL. The stable Firewatch message ID is also the FIFO deduplication ID,
but consumers must still implement durable idempotency beyond SQS’s deduplication
window.
The SQS provider is operational end to end. Product transactions still write an outbox row in PostgreSQL; a relay publishes the stable envelope to SQS and marks that row sent only after AWS acknowledges it. SQS workers long-poll, renew visibility while a handler runs, delete only after the handler and durable processed-message record commit, and leave failures for retry/redrive. A PostgreSQL advisory transaction lock prevents concurrent duplicate deliveries from executing the same handler. This transactional-outbox boundary is intentional: SQS replaces database polling as the managed worker transport, but cannot atomically replace the database outbox.
The web bundle defaults to same-origin API calls. At runtime,
FIREWATCH_API_UPSTREAM tells its Nginx proxy where to forward /api, /health,
and /openapi; the default used by Compose is http://api:8080. This keeps the
same versioned web image portable across hostnames and environments.
VITE_API_BASE_URL remains a development build argument, but authenticated
production web deployments require the same-origin runtime proxy. The API does
not enable credentialed cross-origin browser requests. This avoids cross-site
cookie and CSRF ambiguity and keeps one portable web image across environments.
Future configuration additions should be optional with safe defaults during a compatibility window. Renaming or removing a variable is a deployment-contract change and requires release notes.
Database and storage
Section titled “Database and storage”Firewatch requires a supported PostgreSQL database reachable by both API and
worker. Managed Cloud also uses the private S3 bucket selected through
Runbooks__Provider=S3; the database stores asset metadata while S3 stores
object bytes. Application containers remain disposable. Operators own
encryption, credentials, capacity, high availability, backups, point-in-time
recovery, and restore testing for both stores.
ASP.NET Core Data Protection keys are persisted in the shared Firewatch
PostgreSQL database so cookies and security tokens remain valid across API
replicas and container replacement. Managed deployments must use a stable
Data Protection application name for every API replica and configure
Auth__DataProtection__CertificatePath to a mounted PKCS#12 certificate whose
password comes from Auth__DataProtection__CertificatePassword. The API fails
startup in managed Provisioned mode when this protector is absent. The private cloud
repository owns access policy, rotation, backup, and recovery for that protecting
key. Database encryption alone does not replace key-ring protection, and neither
the key ring nor its protecting private key may be baked into an image.
During certificate rotation, the new certificate becomes the primary protector
and every still-needed old certificate is configured under
PreviousCertificates; remove an old decrypt certificate only after every key,
session, and protected provisioning token that used it has passed its retention
window.
Run migrations as a one-off release task before increasing the new application version’s replica count:
docker run --rm \ --env ConnectionStrings__Firewatch="<secret connection string>" \ firewatch/api:<version> --migrateThe API image entry point is dotnet Firewatch.Api.dll, so --migrate invokes
the product’s migration-only mode, then the migration hook for every configured
trusted extension, and exits nonzero on failure. Private migrations must use a
separate schema and migration-history table. Managed deployments keep
Database__MigrateOnStartup=false to prevent replica races.
Compose sets it to true only so a clean local volume starts with one command.
Migration policy is forward-first and expand/contract:
- Back up and verify restore capability before a production schema change.
- Apply a migration compatible with the currently running version.
- Roll out API and workers gradually; check readiness and processing lag.
- Remove old columns or compatibility code only in a later release.
Never infer that rolling back an image reverses a migration. Release notes must call out any irreversible operation and minimum PostgreSQL version.
HTTP, health, and ingress
Section titled “HTTP, health, and ingress”The API contract is rooted at /api/v1. A native iOS client and the web client
consume the same routes. API version removal requires a separately documented
deprecation window.
| Endpoint | Use |
|---|---|
GET /health/live |
Process liveness; no dependency details |
GET /health/ready |
Traffic readiness; includes PostgreSQL connectivity |
GET /openapi/v1.json |
Versioned OpenAPI document |
The platform should terminate TLS, preserve or assign X-Request-ID, set
forwarded headers from trusted proxies only, and route API readiness independently
from the web container’s /web-health/ready. Health responses must not be used
as authenticated administrative endpoints.
The public API applies per-account or per-invitation authentication throttles and Identity lockout, but deliberately does not trust arbitrary forwarded client-IP headers. Cloud ingress must additionally apply per-client/IP limits to login, registration, and invitation endpoints before traffic reaches the shared web proxy; a replica-wide proxy-address bucket would let one caller deny service to every tenant.
Graceful shutdown and scaling
Section titled “Graceful shutdown and scaling”The platform sends SIGTERM and allows at least 30 seconds for API shutdown and
45 seconds for worker shutdown before SIGKILL. On shutdown:
- the API stops accepting new requests and finishes in-flight requests;
- the worker stops claiming new work, honors cancellation, and either completes or leaves the current item eligible for retry;
- an interrupted or visibility-expired message may be delivered again, so handlers remain idempotent.
API and worker replica counts are independent. PostgreSQL-provider workers rely on database claims to avoid concurrent ownership; SQS workers rely on visibility leases. Scaling does not change message identity or delivery guarantees.
Upgrade acceptance
Section titled “Upgrade acceptance”Before promoting a public release, the private deployment pipeline should:
- verify each image signature/digest and its matching source version;
- run the migration task once and retain its logs;
- wait for API readiness before routing traffic;
- verify worker health, outbox age, retry volume, and queue depth;
- exercise the versioned system test-job flow in a non-production environment;
- confirm structured logs and request/message correlation reach the managed observability system;
- retain the prior image digests and follow the migration-aware rollback plan.
Cloud-only dashboards, alarms, runbooks, release orchestration, provider credentials, and disaster-recovery automation stay private. Fixes to product behavior discovered through those systems flow back to this public repository.