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

site-floats-api

API Gateway (site-floats, AWS IAM)

300s

default

DynamoDB R/W (site-float-service, collection-history, locks, requirements-bypass); EventBridge PutEvents (floatme-events); invoke Payments, Underwriting, Users, TXN, Insight, Subscriptions, LOC; RDS + vendor secrets

site-floats-webhook-worker

SQS (both event taps, see SQS Queues)

810s

512 MB

DynamoDB R/W (collection-history, locks); invoke Payments, Users, TXN, Underwriting; RDS + vendor secrets

site-floats-ach-handler

Kinesis (prod-payments, see Kinesis)

840s

default

DynamoDB R/W (collection-history); invoke Users, Underwriting, Admin API; RDS + vendor secrets

site-floats-batch-worker

SQS (site-floats-batch-worker)

54s

default

DynamoDB R/W (collection-history, locks); invoke Payments, Users, Underwriting; RDS + vendor secrets

site-floats-reporter

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

site-floats-day-before-ach

day-before-ach

Next-day ACH for SCHEDULING floats due the next business day — Collections

site-floats-prenote

prenote

Zero-dollar USIO prenotes for SCHEDULING floats ahead of their due date — Collections

site-floats-initial

initial

Due-date collection run over SCHEDULING floats due today or earlier — Collections

site-floats-daily-retry

daily-retry

Morning retry pass over overdue floats — Collections

site-floats-daily-retry-evening

daily-retry --slot=evening

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

site-floats-ecs-task-execution

Task execution role for every task definition: ECR pull, log routing, read of the site/datadog runtime secret

site-floats-collections-jobs-task

Task role for the container: RDS main/replica, GrowthBook, Slack, and receipt-client (segment/iterable/appsflyer) secrets, collection-history write/query, locks R/W, invoke Payments, Users, Txn, and Underwriting

site-floats-scheduler-invoke-ecs

Assumed by EventBridge Scheduler: ecs:RunTask on the five task definition families and iam:PassRole on the roles above

CLI Flags

All subcommands accept --payment-submit-rate and --workers; daily-retry also accepts --slot:

Flag Default Prenote Default Effect

--payment-submit-rate

5

10

Payment submissions per second (token-bucket rate limiter shared across workers)

--workers

5

5

Worker goroutines consuming from the candidate channel

--slot

morning

—

daily-retry only: morning (full retry pass) or evening (blind pinless attempts only — Collections)

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

site-floats-initial

18

default (5)

site-floats-daily-retry

18

default (5)

site-floats-daily-retry-evening

18

default (5)

site-floats-day-before-ach

default (5)

default (5)

site-floats-prenote

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

site-floats-balance-event-tap

900s

1

Yes

Balance update events (txn-service.feeder, new_account); consumed by the webhook-worker

site-floats-plaid-disconnect-event-tap

900s

1

Yes

Plaid connection lost/revoked events (txn-service.miner); consumed by the webhook-worker

site-floats-batch-worker

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

collection-history

us-east-1 in prod; current region in test

Collection attempt log; read/written by all collection components

locks

us-east-1 in prod; current region in test

Distributed locks (cirello.io/dynamolock); 60s lease, 1s heartbeat

requirements-bypass

us-east-1 in prod; current region in test

Per-user bypass records; written by the API Lambda

site-float-service

us-east-2 (float_service_table_region)

Provisioned and wired to the API via DYNAMO_TABLE, but no code path reads or writes it

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

cron(0 12 ? * * *) UTC, prod only

reporter

Daily origination report to Slack

site-floats-day-before-ach

cron(0 19 ? * MON-FRI *) America/New_York

Fargate day-before-ach

Day-before ACH batch

site-floats-prenote

cron(0 22 ? * SUN,MON,THU,FRI,SAT *) UTC

Fargate prenote

Prenote batch

site-floats-initial

cron(0 6 ? * MON-FRI *) America/New_York

Fargate initial

Due Date batch

site-floats-daily-retry

cron(0 5 ? * MON-FRI *) America/New_York

Fargate daily-retry

Daily Retry batch

site-floats-daily-retry-evening

cron(0 20 ? * THU,FRI *) America/New_York

Fargate daily-retry --slot=evening

Evening blind retry batch

The Fargate schedules set retry_policy.maximum_retry_attempts = 0: a failed or missed ecs:RunTask is never retried and requires operator follow-up.

Event-tap rules routing external events to SQS:

Rule Routing

site-floats-balance-detected

new_account events from txn-service.feeder (main account, non-negative balance) to site-floats-balance-event-tap

site-floats-plaid-disconnect

plaid_item_connection_lost / plaid_item_connection_revoked from txn-service.miner to site-floats-plaid-disconnect-event-tap

Secrets Manager

All secrets are namespaced by environment (site/…​).

Secret Contents Consumers

site/rds/main

RDS main connection credentials

api, ach-handler, webhook-worker, batch-worker, collections-jobs

site/rds/replica

RDS read-replica credentials

All components, for read-only float lookups

site/segment

Segment write key

api, ach-handler, webhook-worker, batch-worker, collections-jobs (initial, daily-retry)

site/iterable

Iterable API key

api, ach-handler, webhook-worker, batch-worker, collections-jobs (initial, daily-retry)

site/appsflyer

AppsFlyer API key

api, ach-handler, webhook-worker, batch-worker, collections-jobs (initial, daily-retry)

site/growthbook

GrowthBook SDK key

api, webhook-worker, collections-jobs

site/slack

Slack webhook URL + bot token

reporter, collections-jobs

site/datadog

Datadog API key (runtime)

Lambda Datadog extension; ECS task startup

site/datadog/terraform

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.