Infrastructure

All infrastructure is AWS, defined in Terraform under deploy/ and applied per target. Resource names carry the site prefix (test, staging, or prod, e.g. prod-txn-api); the application identifier is txn. There are four targets: the old test environment in the shared account (us-west-2), the new test environment in its own account (us-east-2), staging in its own account (us-east-2), and prod in the shared account (us-east-2). The legacy tables live in us-east-1 for new test, staging, and prod and in the deploy region for old test; the legacy_region variable selects it.

Deployment

The workflows are split by trigger: on_pr.yaml plans every continuous target, on_main.yaml applies them, and on_release.yaml applies prod. Each target is one call of the reusable deploy-env.yaml, which takes the account, deploy region, legacy region, state bucket, and artifact bucket as inputs and fetches the tarball built once earlier in the run (or attached to the published release for prod). The Lambda packages are uploaded to the target’s artifact bucket (artifact_bucket variable): floatme-test-media for old test, floatme-test-services-artifacts for new test, floatme-staging-media for staging, and floatme-prod-media for prod. New test also passes two overrides its account needs: bin_list_bucket_name = floatme-test-txn-bin-list-data (the default test-txn-bin-list-data is old test’s bucket) and auth0_event_bus_name = test-auth0 (the account bridges the Auth0 partner source onto its own bus; with the override set, the partner-source lookup is skipped).

The workflow authenticates to AWS with GitHub OIDC; there are no static credentials. Pull requests assume the read-only site-github-services-plan role and run terraform plan; pushes to main and published releases assume site-github-services-deploy and run terraform apply. Both wait up to five minutes for the state lock instead of failing when another run holds it. Both roles, and the site-github-services-boundary permissions boundary carried by every Lambda role and by the site-txn-eventbridge-auth0 rule role, are owned by the infrastructure repo. The Terraform providers use the workflow’s credentials directly, with no further role assumption. State lives in the target’s state bucket (floatme-test-media, floatme-test-services-terraform-state, floatme-staging-services-terraform-state, or floatme-prod-media) under terraform/transactions-service/, locked by the terraform-locking table. The provider lock file deploy/.terraform.lock.hcl is committed; provider upgrades go through it.

Lambda Functions

Function Trigger Timeout Memory Key IAM Permissions

site-txn-api

API Gateway site-txn

300s

1024 MB

DynamoDB R/W transactions, plaid, plaid-account-changes; read institutions; S3 GetObject on the BIN bucket; SQS send plaid-webhooks; EventBridge PutEvents; invoke User, Float, Insight; secrets plaid, growthbook, iterable

site-txn-webhook

API Gateway site-floatme-webhook-api

300s

default

SQS send plaid-webhooks; secret plaid

site-txn-miner

SQS site-txn-plaid-webhooks

810s

default

DynamoDB R/W transactions, plaid, plaid-account-changes; Kinesis PutRecords plaid-transactions; SQS receive plaid-webhooks, send plaid-throttle; EventBridge PutEvents; invoke Admin API, User, LOC, Payments; secrets plaid, growthbook, segment

site-txn-throttle-miner

SQS site-txn-plaid-throttle

810s

256 MB

DynamoDB R/W plaid; Kinesis PutRecords plaid-transactions; SQS receive and send plaid-throttle; invoke User, LOC; secrets plaid, growthbook, segment

site-txn-refiner

Kinesis site-txn-plaid-transactions

840s

512 MB

DynamoDB R/W transactions; Kinesis read; SQS send site-insight-replay-feeder; EventBridge PutEvents

site-txn-feeder

DynamoDB Streams (transactions, plaid)

840s

default

DynamoDB Streams read; DynamoDB R/W plaid; Kinesis PutRecords floatme-transactions; EventBridge PutEvents; invoke User; secret plaid

site-txn-listener

SQS site-txn-listener-eventbridge

840s

512 MB

DynamoDB Query transactions, plaid; SQS receive listener-eventbridge, send plaid-webhooks

site-txn-institution

EventBridge rule site_institution_job_schedule

300s

256 MB

DynamoDB PutItem institutions; secret plaid

The miner and throttle-miner timeouts are 90% of their queue’s 900s visibility timeout.

Event Source Mappings

Mapping Configuration

miner ← site-txn-plaid-webhooks

Batch 10, batching window 2s, ReportBatchItemFailures

throttle-miner ← site-txn-plaid-throttle

Batch 10, ReportBatchItemFailures

listener ← site-txn-listener-eventbridge

Batch 10

refiner ← site-txn-plaid-transactions

Batch 10, parallelization factor 2, AT_TIMESTAMP (apply time minus 15 minutes), bisect on error, ReportBatchItemFailures

feeder ← site-txn-transactions stream

LATEST, bisect on error, ReportBatchItemFailures. Filters: NewImage.type = ACCOUNT; TTL expirations (userIdentity.principalId = dynamodb.amazonaws.com) with OldImage.type = LAYER

feeder ← site-txn-plaid stream

LATEST, bisect on error, ReportBatchItemFailures. Filter: TTL expirations with OldImage.type = ITEM

API Gateways

Gateway Auth Routing

site-txn (this repo)

AWS IAM (SigV4)

ANY /{proxy+} to site-txn-api, 30s integration timeout. Callers are FloatMe backend services and the edge workers that front authenticated mobile requests; the Lambda’s fmsdk/authorizer middleware accepts IAM callers from the deploy account.

site-floatme-webhook-api (shared)

Plaid webhook signature, verified in the Lambda

POST /v1/plaid to site-txn-webhook — Webhook intake

site-txn is also reachable as txn.site.svc.floatme.io (deploy/domain.tf): an alias record in the infrastructure repo’s per-environment site.svc.floatme.io zone, using its wildcard certificate. The execute-api endpoint stays enabled for existing callers.

DynamoDB Tables

All tables are Terraform data sources here; the three prefixed tables are provisioned in the infrastructure repo. Schemas and access patterns are in DynamoDB Tables.

Table Region Streams Used by

site-txn-transactions

deploy region

Yes (feeder)

api, miner, refiner, listener

site-txn-plaid

deploy region

Yes (feeder)

api, miner, throttle-miner, feeder

site-txn-plaid-institutions

deploy region

No

api (read), institution (write)

plaid-account-changes

us-east-1 in prod

No

api, miner (written on item removal and main-account change; read by the change check)

S3

The API Lambda loads the BIN reference list from site-txn-bin-list-data (provisioned in the infrastructure repo) at startup and serves it from memory.

Setting Value

Env vars

BIN_LIST_S3_BUCKET, BIN_LIST_S3_REGION, optional BIN_LIST_S3_PREFIX (empty = bucket root)

Objects

bin-list-data.csv (required; load failure exits with code 76) and bin-aliases.json (optional brand-to-issuer aliases; load failure is logged)

Response cap

9 MB; entries beyond the cap are dropped and truncated is set

Source: cmd/api/main.go, pkg/api/bins.go

SQS Queues

Queue Visibility Delay Max Receive Producers Consumer

site-txn-plaid-webhooks

900s

0s

5

webhook; api and listener (synthetic webhooks)

miner

site-txn-plaid-throttle

900s

30s

5

miner, throttle-miner

throttle-miner

site-txn-listener-eventbridge

900s

0s

5

EventBridge rule site-txn-auth0-login-events

listener

site-txn-unroller-writes

900s

0s

5

None in this repo

None in this repo

site-insight-replay-feeder

Owned by Insight Service

—

—

refiner

Insight Service

Each queue owned here has a <queue>-dlq dead-letter queue.

Kinesis

Stream Producers Consumer Notes

site-txn-plaid-transactions

miner, throttle-miner

refiner

Raw Plaid TransactionsGetResponse wrapped with user_id, item_id, removed, last_page

site-txn-floatme-transactions

feeder (IAM and env vars wired)

None in this repo

The feeder’s stream filter excludes TRANSACTION records, the only type written here; no records are published

Both streams are data sources provisioned in the infrastructure repo.

EventBridge

Publishers target the account default bus in the deploy region; there is no custom bus and EVENTBRIDGE_AWS_BUS_NAME is unset. Contracts are in Events.

Bus / Rule Routing

site-auth0 bus (prod); Auth0 partner event source (test)

Auth0 log-stream events

site-txn-auth0-login-events rule

detail.data.type in gd_auth_succeed, sepft, sertft from the deploy account, to site-txn-listener-eventbridge

Schedules

Rule Cron Target

site_institution_job_schedule

cron(0 0 ? * 7 *) — Saturday 00:00 UTC

site-txn-institution

AWS cron day-of-week 7 is Saturday. The rule’s Terraform description says Sunday; the cron is what runs.

Secrets Manager

Secret Contents Consumers

site/plaid

Plaid client ID and secret

api, webhook, miner, throttle-miner, feeder, institution

site/growthbook

GrowthBook SDK key and host

api, miner, throttle-miner

site/segment

Segment write key

miner, throttle-miner

site/iterable

Iterable API key

api

site/datadog/terraform

Datadog API and app keys

Terraform (SLOs, service definition)

Runtime Configuration

Behavior-affecting environment variables set by Terraform.

Variable Lambda test prod Effect

DAYS_BETWEEN_PLAID_ITEM_CHANGES

api

7

7

Cooldown in Change Check

MINUTES_BEFORE_PLAID_ITEM_CHANGE

api

0

1440

Account age below which a v1 removal by a user with detected employment is banned

LOG_ONLY_PLAID_ITEM_BAN

api

false

true

Ban is logged instead of calling the User Service

ALLOW_ITEM_CHANGE_WITHOUT_EMPLOYMENT

api

true

true

Change check passes when Insight reports no employment

REACTIVATION_ITEM_TTL_MINS

api

5

1440

TTL on items added with is_reactivating

PREQUAL_ITEM_TTL_DAYS

api

7

7

TTL on prequal items

ACCOUNT_BALANCE_TTL_DAYS

refiner, api

100

100

Balance snapshot TTL

ENABLE_LIABILITY_MINING

miner

true

false

Process LIABILITIES webhooks

PLAID_MINE_PERSONAL_FINANCE_CATEGORY, PLAID_MINE_LOGO_AND_COUNTERPARTY

miner, throttle-miner

false

false

Set by Terraform; no code path reads them

WEBHOOK_ALLOW_LIST, PERMITTED_USERIDS

miner

false, unset

false, unset

Read into the miner struct; no code path evaluates them

WEBHOOK_HOUR_ENV

listener

24h

24h

Read into the listener struct; the 24-hour freshness window is a code constant

Source: deploy/lambda.tf, deploy/variables.tf

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.

Three Datadog SLOs (Lambda errors, throughput, latency) span all eight functions, each 99.9% target / 99.99% warning over 7-day and 30-day windows. A Datadog service definition is published from deploy/datadog.tf.