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
- "Missing the following required SSO configuration values: sso_start_url, sso_region" — the profile is unconfigured. Run
aws configure sso --use-device-code. - Browser flow hangs on a Cloud Desktop — the default flow opens
http://127.0.0.1:<port>/oauth/callback, which the host's browser can't reach. Always use--use-device-codeon remote machines. Token has expired and refresh failed— re-runaws sso login --profile fraserbenjamin.
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.