TailSpend Docs

AWS access

TailSpend deploys to a single AWS account from a developer workstation. CDK and the AWS CLI both authenticate against AWS IAM Identity Center (SSO) — there are no long-lived IAM access keys.

This page covers the one-time SSO setup and the day-to-day login flow.

Account

Field Value
AWS account 177754533438
SSO start URL https://fraserbenjamin.awsapps.com/start
SSO region eu-west-2
Default CLI region us-east-1 (where TailSpend is deployed)
Role Administrators

The SSO portal is hosted in eu-west-2; deployed resources (Lambda, API Gateway, DynamoDB, Cognito) all live in us-east-1.

One-time setup

Run aws configure sso to register a named profile and an SSO session. On a Cloud Desktop or any headless machine, pass --use-device-code so the CLI prints a device code instead of trying to open a local browser (the default browser flow uses a 127.0.0.1 callback that won't work over SSH):

aws configure sso --use-device-code

Answer the prompts:

SSO session name (Recommended): fraserbenjamin-sso
SSO start URL [None]: https://fraserbenjamin.awsapps.com/start
SSO region [None]: eu-west-2
SSO registration scopes [sso:account:access]: <enter>

# device-code prompt: open the printed URL on any machine, paste the code,
# sign in, and approve the request.

Default client Region [None]: us-east-1
CLI default output format (json if not specified) [None]: <enter>
Profile name [Administrators-177754533438]: fraserbenjamin

This writes two blocks to ~/.aws/config:

[sso-session fraserbenjamin-sso]
sso_start_url = https://fraserbenjamin.awsapps.com/start
sso_region = eu-west-2
sso_registration_scopes = sso:account:access

[profile fraserbenjamin]
sso_session = fraserbenjamin-sso
sso_account_id = 177754533438
sso_role_name = Administrators
region = us-east-1

Verify the profile works:

aws sts get-caller-identity --profile fraserbenjamin

You should see an arn:aws:sts::177754533438:assumed-role/AWSReservedSSO_Administrators_* ARN.

Daily use

SSO sessions expire (typically every ~8 hours). When a CLI or CDK command starts failing with Token has expired and refresh failed, log back in:

aws sso login --profile fraserbenjamin

Re-run with --use-device-code if you're on a headless host:

aws sso login --profile fraserbenjamin --use-device-code

Using the profile

Either pass --profile fraserbenjamin per command, or export it for the shell session:

export AWS_PROFILE=fraserbenjamin

CDK picks the profile up the same way:

AWS_PROFILE=fraserbenjamin npm run deploy --workspace=@repo/infra

Deploying a specific stage

main.ts resolves the target stage from CDK context (stage), defaulting to dev. Override it with -c stage=.... The flag goes after -- so npm forwards it to cdk:

# dev (default)
AWS_PROFILE=fraserbenjamin npm run deploy --workspace=@repo/infra

# beta or prod
AWS_PROFILE=fraserbenjamin npm run deploy --workspace=@repo/infra -- -c stage=beta
AWS_PROFILE=fraserbenjamin npm run deploy --workspace=@repo/infra -- -c stage=prod

Each invocation only synthesizes one stage, so to deploy multiple stages run the command once per stage.

Deploying a single stack

npm run deploy deploys all four stacks (Dns, Data, Auth, Service) for the resolved stage. To target one stack, use cdk deploy directly:

AWS_PROFILE=fraserbenjamin npm run cdk --workspace=@repo/infra -- deploy TailSpend-dev-Auth

List the stacks for the current stage:

AWS_PROFILE=fraserbenjamin npm run cdk --workspace=@repo/infra -- list

Troubleshooting

Scan processing and the SQS free tier

Scan submissions invoke tailspend-{stage}-scan-job-worker with Lambda's asynchronous Event invocation type. A 202 confirms durable acceptance into Lambda's internal queue; there is no continuously polled SQS source for new jobs. The producer has lambda:InvokeFunction permission on only its stage's worker.

The worker reserves 10 concurrent executions, retries function errors twice, and retains asynchronous events for at most 16 minutes, matching the processing lease rather than Lambda's six-hour default. Exhausted events go to the existing tailspend-{stage}-scan-jobs-dlq (14-day retention). Unlike the former one-day SQS source retention, Lambda's async event age cannot exceed six hours. Completed/failed job records remain idempotent on sequential redelivery. Monitor Lambda Errors, AsyncEventAge, AsyncEventsDropped and DeadLetterErrors, and the DLQ's ApproximateNumberOfMessagesVisible when diagnosing failures.

The retired scan SQS queues and disabled event source mappings are removed in issue #699. Before removal, all three stages were verified to have disabled mappings and zero visible, in-flight or delayed messages (17 October 2026). The live async worker and its DLQ remain. Replay a DLQ message by invoking the worker asynchronously with the original {userId, jobId, traceHeaders?} JSON body. Do not wrap it in an SQS Records event. Replays of settled jobs are no-ops; leases, saved OCR/model output and recovery checks prevent duplicate paid work.

The September migration retained these queues temporarily for rollback. That rollback window is over. Restoring the old SQS producer would now require a separate reviewed infrastructure change, rather than enabling an idle poller.

September 2026 incident

On September 12, Free Tier reported 859,289 of 1,000,000 SQS requests used, forecasting 2,148,222.5 for the month. Cost Explorer attributed 807,554 requests to receives and 51,490 to attribute reads, versus only 96 sends. US East (N. Virginia) accounted for 758,280 requests; London accounted for 101,009. TailSpend's three scan workers generated approximately 64,800 empty receives per day. Changing only their maximum concurrency would leave substantial idle traffic and would not protect the remaining allowance this month.

With the owner's approval, the unused Mental Fitness project's DynamoDB and SQS mappings in eu-west-2 were also paused (queues empty, no messages sent that month). Their IDs are 9014a842-cd40-4acf-a723-b1b4f97fc667 (DynamoDB) and 376c2cd5-13af-40b0-81e9-3fe5eb13a21f (SQS). These are separate from TailSpend's CDK stacks. Re-enable only when resuming that project; DynamoDB stream records expire after 24 hours, so pausing the stream consumer is not long-term event storage. SQS requests already consumed this month cannot be undone. Future scan traffic should use SQS only when a failed asynchronous job reaches its DLQ.

View Markdown source