Infrastructure
All infrastructure is AWS, defined in Terraform under deploy/ and managed per environment. Resource names are shown with the site prefix (test or prod, e.g. prod-subscription-service-api). The application identifier is subscription-service. Every resource carries the provider default tags floatme:environment, floatme:application, floatme:managed-by (terraform) and floatme:repository (floatme-corp/subscription-service), alongside the unprefixed environment and application keys.
Deployment
Every environment is one call of the shared .github/workflows/deploy-env.yaml, which takes the account, regions, state bucket and artifact buckets as inputs. Pull requests (on_pr.yaml) build once and plan each continuous environment from that build with the read-only site-github-services-plan role; pushes to main (on_main.yaml) build once and apply the same artifacts to each continuous environment with site-github-services-deploy; published releases (on_release.yaml) apply prod from the release’s artifacts. Authentication is GitHub OIDC; there are no static credentials. Both roles, and the site-github-services-boundary permissions boundary every Lambda role carries, are owned by the infrastructure repo. The Terraform providers use the workflow’s credentials directly, with no further role assumption.
| Target | Account | Region | Legacy region | State bucket | Artifact buckets (primary / legacy) |
|---|---|---|---|---|---|
test (old account) |
267052520423 |
us-west-2 |
us-west-2 |
|
|
test |
299185625033 |
us-east-2 |
us-east-1 |
|
|
staging |
208295047603 |
us-east-2 |
us-east-1 |
|
|
prod |
267052520423 |
us-east-2 |
us-east-1 |
|
|
State is keyed terraform/subscription-service/ in each state bucket and locked by the terraform-locking table in each account. Lambda packages upload to the artifact bucket (artifact_bucket); the kinesis-feeder Lambda, which runs in the legacy region next to the DynamoDB stream, uploads to the legacy artifact bucket (legacy_artifact_bucket). deploy/.terraform.lock.hcl pins the providers.
Peer service APIs (admin, user-service, txn, insight, underwriting, entitlements) are resolved by name at plan time and addressed by convention (https://{id}.execute-api.{region}.amazonaws.com). A peer that has not yet deployed to an environment yields an empty URL and an account-wide execute-api:Invoke grant until the next deploy picks up its id, so a fresh environment comes up in two passes rather than failing the plan. The payments API is a REST gateway with no list-by-name lookup, so payments-service must deploy to an environment before this service.
Lambda Functions
| Function | Trigger | Timeout | Memory | Key IAM Permissions |
|---|---|---|---|---|
|
API Gateway ( |
900s |
512 MB |
DynamoDB R/W/delete (billing-activity, billing-activity-history, locks); invoke Payments, Users, TXN, Underwriting; Segment, GrowthBook, AppsFlyer secrets |
|
SQS |
54s |
default |
DynamoDB R/W (all three tables); SQS on the three collection queues; invoke TXN, Payments, Users, Insight; SageMaker InvokeEndpoint; Segment, GrowthBook, Iterable, AppsFlyer secrets |
|
EventBridge rules (EventBridge); SQS paging queues (batch size 1) |
810s |
2048 MB |
SQS SendMessage (three collection queues); SQS full access on paging queues; DynamoDB Query (billing-activity) |
|
Kinesis |
840s |
default |
Kinesis read (users stream); DynamoDB R/W + BatchWriteItem (all three tables); invoke Users |
|
Kinesis |
840s |
default |
Kinesis read (payments stream); DynamoDB R/W + BatchWriteItem (billing-activity, billing-activity-history); invoke Users, Admin, TXN; Segment, Iterable, AppsFlyer secrets |
|
DynamoDB Stream on |
840s |
default |
Kinesis PutRecord(s) ( |
|
SQS |
54s |
default |
DynamoDB R/W (all three tables); SQS on its queue; invoke Payments, Users; Segment secret |
|
SQS |
810s |
default |
DynamoDB R/W (all three tables); SQS on its queue; invoke Payments, Users; Segment, GrowthBook, AppsFlyer secrets |
|
EventBridge |
810s |
default |
DynamoDB Query (billing-activity); SQS on both notifier queues |
|
SQS |
810s |
default |
Invoke TXN, Payments, Users; SQS on its queue; Iterable, GrowthBook secrets |
Timeouts are set in deploy/lambda.tf (SQS-consuming Lambdas run at 90% of their queue’s visibility timeout). Kinesis consumers use batch size 100 (var.kinesis_stream_batch_size), starting position LATEST, and BisectBatchOnFunctionError; the ach-handler additionally sets parallelization factor 2 and ReportBatchItemFailures.
Container Images
The subscription-jobs image (cmd/subscription-jobs/Dockerfile, distroless) is built once per push to main (see Deployment) and pushed to ghcr.io/floatme-corp/subscription-service/subscription-jobs tagged v0.0.{short sha}-alpha and latest. Each apply copies that tag into the account’s site-subscriptions ECR repository — owned by the infrastructure repo, alongside the site-subscriptions ECS cluster — with crane copy, digest unchanged. When release-please creates a release the same manifest also gets the release tag, which the prod apply copies. Alpha images on ghcr beyond the newest ten are deleted after each main apply; release-tagged ones are kept. The image is the entrypoint for scheduled batch jobs run as ECS Fargate tasks; no task definitions reference it yet, and its only subcommand is version. Local builds: make container-build-subscription-jobs.
DynamoDB Tables
Schemas, access patterns, and enums are in DynamoDB Tables.
| Table | Region | Streams | Notes |
|---|---|---|---|
|
|
Yes |
Primary subscription state; the stream drives kinesis-feeder → |
|
|
No |
History records written alongside billing-activity |
|
|
No |
Distributed locks (schema); acquired by all collection Lambdas |
The three tables carry no environment prefix; a dedicated aws.dynamodb provider alias deploys resources and reads data sources in their region.
SQS Queues
| Queue | Visibility | Max Receive | DLQ | Purpose |
|---|---|---|---|---|
|
60s |
5 |
Yes |
Scheduled collection jobs from collections-job; consumed by collections-worker (batch 10) |
|
60s |
5 |
Yes |
Retry collection jobs; consumed by collections-worker (batch 10) |
|
60s |
5 |
Yes |
Pause-state collection jobs; consumed by collections-worker (batch 10) |
|
900s |
5 |
Yes |
collections-job continuation messages for the scheduled run’s paginated scan |
|
900s |
5 |
Yes |
collections-job continuation messages for the retry run |
|
900s |
5 |
Yes |
collections-job continuation messages for the pause run |
|
60s |
— |
No |
Manual batch collection requests; consumed by batch-worker (batch 10) |
|
900s |
1 |
Yes |
Balance events from EventBridge (EventBridge); consumed by webhook-balance-worker (batch 10; 20s delivery delay in test) |
|
900s |
5 |
Yes |
notifier-scheduler paging queue (batch 1) |
|
900s |
5 |
Yes |
Per-user notification jobs; consumed by notifier-worker (batch 10) |
Each DLQ is named <queue>-dlq (900s visibility). The collections-job paging queues let each run continue a paginated DynamoDB scan across invocations: an invocation processes up to PAGES_PER_INVOKE pages, then sends itself a continuation message.
Kinesis Streams
| Stream | Direction | Role |
|---|---|---|
|
Inbound |
Membership lifecycle events from the User Service; consumed by memberships, filtered to 12 event types (event filter) |
|
Inbound |
ACH settlement events from the Payments Service; consumed by ach-handler, filtered to 4 subscription event types (ACH callbacks) |
|
Outbound |
Subscription change feed for downstream services; kinesis-feeder publishes every |
EventBridge
| Rule | Schedule / Pattern | Enabled | Purpose |
|---|---|---|---|
|
|
prod only |
Triggers collections-job with process |
|
|
prod only |
Triggers collections-job with process |
|
|
always |
Triggers collections-job with process |
|
|
prod only |
Triggers notifier-scheduler with status |
|
Pattern: |
always |
Routes balance events to the |
Secrets Manager
Secrets are namespaced by environment (site/…).
| Secret | Purpose | Consumers |
|---|---|---|
|
Segment write key |
api, collections-worker, ach-handler, webhook-balance-worker, batch-worker |
|
Iterable API key |
ach-handler, notifier-worker |
|
GrowthBook SDK key |
api, collections-worker, webhook-balance-worker, notifier-worker |
|
AppsFlyer API key |
api, ach-handler, webhook-balance-worker |
|
Datadog API/app keys |
Terraform only (SLOs and service catalog) |
API Gateway
The site-subscription-service API Gateway (AWS IAM / SigV4 auth) forwards all routes (ANY /{proxy+}) to the site-subscription-service-api Lambda (payload format 1.0, 30s integration timeout). Access logs go to CloudWatch Logs (/aws/api-gateway/site-subscription-service, 30-day retention). Callers are FloatMe backend services using IAM-signed requests.
The gateway is also reachable at the custom domain subscription-service.site.svc.floatme.io, an API Gateway v2 domain mapped to the stage with an alias record in the infrastructure-owned site.svc.floatme.io zone, using that zone’s wildcard ACM certificate. The default execute-api endpoint stays enabled.
Monitoring
Datadog monitors and SLOs exist in prod only (local.create_datadog_monitors in deploy/datadog.tf). Alerts outside prod go unread, and the old and new test accounts both deploy as test, so Datadog would reject the second account’s identical monitors as duplicates.
Datadog SLOs (all 99.9% target / 99.99% warning over 7-day and 30-day windows; service:subscription-service, env:site), defined in deploy/datadog.tf:
| SLO | Lambdas covered |
|---|---|
|
collections-worker, collections-job, memberships, ach-handler, api, kinesis-feeder, notifier-scheduler, notifier-worker |
|
collections-job, memberships, ach-handler, api, kinesis-feeder, notifier-scheduler |
|
ach-handler, api, kinesis-feeder, notifier-scheduler |
The Datadog service catalog entry registers the service as tier 1 (team devops) with links to the source repo and the documentation site.