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-floats-api). The application identifier is floats. All Lambdas and Fargate tasks run inside the shared FloatMe VPC (private subnets, PrivateSG security group) for RDS access.
Deployment
Three workflows drive deploys, each calling .github/workflows/deploy-env.yaml once per target with that target’s account, region, state bucket, artifact bucket and legacy region as inputs. on_pr.yaml builds the Lambda tarball once and runs terraform plan against every continuous target with the read-only site-github-services-plan role. on_main.yaml builds the tarball and the collections-jobs image once, applies every continuous target with site-github-services-deploy, and attaches the tarball to the draft release when release-please creates one. on_release.yaml applies prod from the published release’s tarball and image tag. The continuous targets are the old test environment (account 267052520423, us-west-2), the new test account (299185625033, us-east-2) and staging (208295047603, us-east-2); prod is 267052520423 in us-east-2. Authentication is GitHub OIDC; there are no static credentials. Both roles, and the site-github-services-boundary permissions boundary every Lambda and Fargate role carries, are owned by the infrastructure repo. The Terraform providers use the workflow’s credentials directly, with no further role assumption. State lives in the floatme-site-media bucket under terraform/floats/ for old test and prod, in floatme-test-services-terraform-state for the new test account and in floatme-staging-services-terraform-state for staging, locked by the terraform-locking table in each account. Lambda packages upload to the bucket passed as artifact_bucket (floatme-test-services-artifacts in the new test account, floatme-site-media elsewhere). The provider versions are pinned by the committed deploy/.terraform.lock.hcl.
Peer service APIs (admin, user-service, underwriting, txn, insight, subscription-service, loc-service) 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 be deployed to an environment before this service.
Lambda Functions
| Function | Trigger | Timeout | Memory | Key IAM Permissions |
|---|---|---|---|---|
|
API Gateway ( |
300s |
default |
DynamoDB R/W ( |
|
SQS (both event taps, see SQS Queues) |
810s |
512 MB |
DynamoDB R/W (collection-history, locks); invoke Payments, Users, TXN, Underwriting; RDS + vendor secrets |
|
Kinesis ( |
840s |
default |
DynamoDB R/W (collection-history); invoke Users, Underwriting, Admin API; RDS + vendor secrets |
|
SQS ( |
54s |
default |
DynamoDB R/W (collection-history, locks); invoke Payments, Users, Underwriting; RDS + vendor secrets |
|
EventBridge schedule (see EventBridge) |
900s |
128 MB |
RDS replica + Slack secrets |
Fargate
Scheduled batch jobs run as ECS Fargate tasks on the site-floats cluster (awsvpc network mode, FARGATE launch type, platform LATEST, no public IPs). All tasks run the collections-jobs image and dispatch on a subcommand.
| Task Definition | Subcommand | Purpose |
|---|---|---|
|
|
Next-day ACH for |
|
|
Zero-dollar USIO prenotes for |
|
|
Due-date collection run over |
|
|
Morning retry pass over overdue floats — Collections |
|
|
Thu/Fri evening blind-only retry pass — Collections |
Each task is provisioned at 512 CPU / 1024 MB and runs three containers: the application binary, a FireLens log-router sidecar (logs ship to Datadog intake, no CloudWatch Logs group), and a Datadog Agent sidecar (APM and DogStatsD). Schedules are in EventBridge.
Image and Deploy
The collections-jobs image (cmd/collections-jobs/Dockerfile, distroless runtime) is built once per push to main (see Deployment) and pushed to ghcr.io/floatme-corp/float-service/collections-jobs tagged v0.0.{short sha}-alpha. Each apply copies that tag into the account’s site-float-service/collections-jobs ECR repository with crane copy, digest unchanged, and Terraform points each task definition at ${ecr_repo_url}:${var.service_version}. 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. ECR lifecycle: untagged images expire after 7 days; the most recent 5 images are retained.
IAM Roles
| Role | Purpose |
|---|---|
|
Task execution role for every task definition: ECR pull, log routing, read of the |
|
Task role for the container: RDS main/replica, GrowthBook, Slack, and receipt-client (segment/iterable/appsflyer) secrets, |
|
Assumed by EventBridge Scheduler: |
CLI Flags
All subcommands accept --payment-submit-rate and --workers; daily-retry also accepts --slot:
| Flag | Default | Prenote Default | Effect |
|---|---|---|---|
|
5 |
10 |
Payment submissions per second (token-bucket rate limiter shared across workers) |
|
5 |
5 |
Worker goroutines consuming from the candidate channel |
|
|
— |
|
Effective throughput is min(--payment-submit-rate, workers / submit round-trip): the token bucket cannot be reached without enough workers in flight to keep it saturated. Values actually passed by the task definitions:
| Task Definition | --workers |
--payment-submit-rate |
|---|---|---|
|
18 |
default (5) |
|
18 |
default (5) |
|
18 |
default (5) |
|
default (5) |
default (5) |
|
default (5) |
default (10) |
Source: cmd/collections-jobs/main.go for the defaults, deploy/fargate_collections_jobs*.tf for the overrides
SQS Queues
| Queue | Visibility Timeout | Max Receive | DLQ | Purpose |
|---|---|---|---|---|
|
900s |
1 |
Yes |
Balance update events ( |
|
900s |
1 |
Yes |
Plaid connection lost/revoked events ( |
|
60s |
— |
No |
Batch collection requests; consumed by the batch-worker |
Each DLQ is named <queue>-dlq.
Kinesis
The ach-handler consumes the Payments Service’s prod-payments stream, filtered to six event types: FLOAT_DEBIT_COMPLETED, FLOAT_DEBIT_COMPLETED_PLAID, FLOAT_DEBIT_RETURNED, FLOAT_CREDIT_COMPLETED, FLOAT_CREDIT_RETURNED, FLOAT_DEBIT_CHARGED_BACK. Consumer config: batch size 100, parallelization factor 2, starting position LATEST, BisectBatchOnFunctionError enabled.
DynamoDB Tables
Schemas and access patterns are in DynamoDB Tables.
| Table | Region | Notes |
|---|---|---|
|
us-east-1 in prod; current region in test |
Collection attempt log; read/written by all collection components |
|
us-east-1 in prod; current region in test |
Distributed locks ( |
|
us-east-1 in prod; current region in test |
Per-user bypass records; written by the API Lambda |
|
us-east-2 ( |
Provisioned and wired to the API via |
The three us-east-1 tables carry no environment prefix and are shared with other services. Client configuration: the three shared tables resolve their region from DYNAMO_LEGACY_REGION (the lock client separately from DYNAMO_LOCK_REGION); site-float-service from DYNAMO_REGION + DYNAMO_TABLE.
EventBridge
The API Lambda publishes user_float_created to the floatme-events bus — see Events. All scheduled triggers:
| Schedule | Cron | Target | Run |
|---|---|---|---|
|
|
reporter |
Daily origination report to Slack |
|
|
Fargate |
Day-before ACH batch |
|
|
Fargate |
Prenote batch |
|
|
Fargate |
Due Date batch |
|
|
Fargate |
Daily Retry batch |
|
|
Fargate |
Evening blind retry batch |
|
The Fargate schedules set |
Event-tap rules routing external events to SQS:
| Rule | Routing |
|---|---|
|
|
|
|
Secrets Manager
All secrets are namespaced by environment (site/…).
| Secret | Contents | Consumers |
|---|---|---|
|
RDS main connection credentials |
api, ach-handler, webhook-worker, batch-worker, collections-jobs |
|
RDS read-replica credentials |
All components, for read-only float lookups |
|
Segment write key |
api, ach-handler, webhook-worker, batch-worker, collections-jobs (initial, daily-retry) |
|
Iterable API key |
api, ach-handler, webhook-worker, batch-worker, collections-jobs (initial, daily-retry) |
|
AppsFlyer API key |
api, ach-handler, webhook-worker, batch-worker, collections-jobs (initial, daily-retry) |
|
GrowthBook SDK key |
api, webhook-worker, collections-jobs |
|
Slack webhook URL + bot token |
reporter, collections-jobs |
|
Datadog API key (runtime) |
Lambda Datadog extension; ECS task startup |
|
Datadog API + app keys |
Terraform (SLOs, service catalog) |
API Gateway
The site-floats API Gateway (AWS IAM / SigV4 auth) forwards all routes (ANY /{proxy+}) to the site-floats-api Lambda. Callers are FloatMe backend services and the global FloatMe API Gateway for authenticated mobile requests.
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 cover the api and ach-handler Lambdas: error and throughput SLOs on both — each 99.9% target / 99.99% warning over 7-day and 30-day windows.