AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证S3 object storage built for the AI era
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Full-stack prototype — pnpm workspaces monorepo deploying to AWS via SST v3.
• Node.js >= 24
• AWS CLI — required for S3 sync and CloudFront invalidation during deploy. Install guide
• SSO Session name: filone-sandbox
• SSO start URL: https://d-9067ff87d6.awsapps.com/start
• SSO region: us-east-1
• SSO registration scopes: sso:account:access
• Account ID: 654654381893
• Role: AdministratorAccess
• Default region: us-east-2 - Or whatever region you want.
• Output format: json
2. Log in and activate the profile
MUST do this before you can deploy.查看完整 README ↗Full-stack prototype — pnpm workspaces monorepo deploying to AWS via SST v3.
hyperspace/
├── sst.config.ts # SST v3 infrastructure (app stack — API, website, queues, etc.)
├── infra/ # SST v3 infrastructure (base infra — OIDC provider, IAM roles)
├── packages/
│ ├── shared/ # TypeScript interfaces shared between website and backend
│ ├── aurora-backoffice-client/ # Generated TS client for Aurora Back Office API
│ ├── aurora-portal-client/ # Generated TS client for Aurora Portal API
│ ├── backend/ # Lambda handlers (upload → DynamoDB)
│ └── website/ # Vite + React 19 + TanStack Router SPA + Tailwind v4
| Staging/dev Account | 654654381893 |
| Region | us-east-2 |
| SSO portal | https://d-9067ff87d6.awsapps.com/start |
1. Configure the AWS profile (one-time)
aws configure sso --profile filone
When prompted:
filone-sandboxhttps://d-9067ff87d6.awsapps.com/startus-east-1sso:account:access654654381893AdministratorAccessus-east-2 - Or whatever region you want.json2. Log in and activate the profile
MUST do this before you can deploy.
aws sso login --profile filone
Then set the profile for your shell session so SST picks it up:
export AWS_PROFILE=filone
To make this permanent, add it to your shell config:
# Add to ~/.zshrc (or ~/.bashrc)
echo 'export AWS_PROFILE=filone' >> ~/.zshrc
source ~/.zshrc
You can verify it's working with:
aws sts get-caller-identity
3. Install dependencies
pnpm install
4. Set SST secrets (one-time per stage)
pnpx sst secret set Auth0ClientId <value> [--stage <stage>]
pnpx sst secret set Auth0ClientSecret <value> [--stage <stage>]
pnpx sst secret set Auth0MgmtClientId <value> [--stage <stage>]
pnpx sst secret set Auth0MgmtClientSecret <value> [--stage <stage>]
pnpx sst secret set Auth0MgmtRuntimeClientId <value> [--stage <stage>]
pnpx sst secret set Auth0MgmtRuntimeClientSecret <value> [--stage <stage>]
pnpx sst secret set StripeSecretKey <value> [--stage <stage>]
pnpx sst secret set StripePriceId <value> [--stage <stage>]
pnpx sst secret set AuroraBackofficeToken <value> [--stage <stage>]
pnpx sst secret set SendGridApiKey <value> [--stage <stage>]
pnpx sst secret set HubSpotServiceKey <value> [--stage <stage>]
pnpx sst secret set GrafanaLokiAuth '<instanceId>:<apiKey>' [--stage <stage>]
pnpx sst secret set DeletionCodeHmacKey <value> [--stage <stage>]
Omit --stage to set for your personal dev stage (defaults to OS username).
There are two Auth0 M2M credentials with different scopes — see docs/Auth0OneTimeSetup.md. The AuroraBackofficeToken is from the Aurora Back Office dashboard — see the API token section below. The GrafanaLokiAuth secret is from Grafana Cloud — see the Observability section below.
DeletionCodeHmacKey is the only entry with no dashboard to copy from — it is our own key, not a vendor credential, so generate it with openssl rand -hex 32. It keys the account-deletion code HMAC, and rotating it invalidates every code already issued on that stage. It has no default and is linked on every stage, so sst deploy fails without it; staging and production therefore need it set before any branch that links it reaches main, since both deploy automatically from there. CI preview stages generate their own.
pnpm run dev # SST live dev mode (live Lambda debugging)
pnpm run build # Build all packages
pnpm run deploy:dev # Build and deploy personal dev stack (uses OS username as stage)
pnpm run deploy:local # Build and deploy to a local AWS emulator (see Local Stack)
pnpm run remove # Remove your personal dev stack
pnpm run test # Lint, then run the unit tests
pnpm run test:unit # Run every package's unit tests without linting (CI lints in its own job)
pnpm run storybook # Start Storybook dev server on port 6006
pnpm run test:storybook # Run Storybook tests (browser-based, requires Playwright)
pnpm run lint # Lint and typecheck TypeScript code (via oxlint)
pnpm run lint:fix # Lint and auto-fix where possible
Do not run
deploy:stagingordeploy:productionmanually. Staging and production deployments should go through CI/CD.
# Local website dev server (for frontend-only changes)
cd packages/website && pnpm run dev
Playwright end-to-end test suite under tests/e2e/ that runs against a deployed environment (typically staging). The @playwright/test package is already a devDependency, so pnpm install covers it.
Install browser binaries (one-time):
pnpm exec playwright install --with-deps
| Variable | Purpose |
|---|---|
BASE_URL | The deployed app URL (e.g. https://staging.fil.one) |
E2E_PAID_EMAIL / E2E_PAID_PASSWORD / E2E_PAID_USER_ID | Paid test user (_USER_ID is the FilOne user id) |
E2E_UNPAID_EMAIL / E2E_UNPAID_PASSWORD / E2E_UNPAID_USER_ID | Unpaid test user |
E2E_TRIAL_EMAIL / E2E_TRIAL_PASSWORD / E2E_TRIAL_USER_ID | Trial test user |
E2E_DISABLED_EMAIL / E2E_DISABLED_PASSWORD / E2E_DISABLED_USER_ID | Disabled test user (see Disabled test account) |
In CI, all twelve credential vars come from GitHub repository secrets. The post-deploy run (.github/workflows/test-staging.yaml) and the nightly run (.github/workflows/e2e-staging.yaml) both call .github/workflows/e2e-suites.yaml, which holds the Playwright setup and also configures AWS via OIDC (using vars.AWS_ROLE_ARN), so the setup can invoke the staging AccountStateTestHelper Lambda and the org specs can seed their DynamoDB rows. Each suite (destructive with smoke, multi-org, policies) runs as its own job with its own accounts; each job runs Chromium, Firefox and WebKit one after another. A suite's jobs in both workflows share an e2e-staging-<suite> concurrency group, so only one run of a suite touches staging at a time. A nightly or manual run of e2e-staging.yaml skips itself while a push to main has reached deploy-staging and its post-deploy E2E jobs have not finished, because those jobs test the same suites.
The bucket/upload tests in tests/e2e/destructive/buckets.spec.ts run once per S3 region (eu-west-1 and us-east-1, see tests/e2e/destructive/regions.util.ts) and reuse existing buckets rather than creating them (the account-wide bucket limit is 100 and buckets are not yet deletable). Each test account (paid, unpaid, trial and disabled) must therefore have at least one bucket in each region on the target stage; otherwise the test fails with a No <region> bucket found error.
Seeding is automatic for all four accounts: the destructive-seed-buckets Playwright project (tests/e2e/destructive/buckets.setup.ts) runs after destructive-setup and calls ensureBucketInEachRegion (tests/e2e/destructive/buckets.util.ts), which creates a bucket only for regions where the account has none. This keeps the suite working when a stage resets its storage layer and all buckets disappear; creation stays conditional so repeated runs do not leak buckets toward the 100-bucket limit.
The unpaid account is past_due, and the subscription guard rejects write requests in that state with 403 GRACE_PERIOD_WRITE_BLOCKED. For that role the seeding step switches the account to the paid state (setAccountState in tests/e2e/destructive/account-state.util.ts), creates the missing buckets, then restores the unpaid state in a finally block — before any spec runs, so the dashboard and upload tests still see the write-blocked state. The disabled account takes the same detour from its disabled state.
If the bucket list itself fails (GET /api/buckets returning a 5xx, e.g. when a stage's per-tenant S3 credentials in SSM have gone stale), the seeding step fails loudly with the status and response body rather than mistaking the error for an empty account — and because the browser projects depend on it, the whole run stops.
The upload tests report backend failures the same way. An upload is two round-trips — POST /api/presign, then a PUT straight to the region's S3 endpoint (getS3Endpoint in packages/shared/src/constants.ts) — and neither failure navigates anywhere, so submitUploadExpectingSuccess reads both responses and fails with the region, status and response body. Without it, a region whose storage backend returns a 502 is indistinguishable from a hung browser: both surface only as a toHaveURL timeout on the navigation back to the bucket page. A rejected PUT has taken ~30s to come back, so these tests get a longer timeout than the default.
The test:e2e script wraps Playwright in sst shell so SST Resource bindings (e.g. the AccountStateTestHelper function name) resolve to the current SST stage. Deploy a stage first, then:
SST_STAGE=staging \
BASE_URL=https://staging.fil.one \
E2E_PAID_EMAIL=... E2E_PAID_PASSWORD=... E2E_PAID_USER_ID=<uuid> \
E2E_UNPAID_EMAIL=... E2E_UNPAID_PASSWORD=... E2E_UNPAID_USER_ID=<uuid> \
E2E_TRIAL_EMAIL=... E2E_TRIAL_PASSWORD=... E2E_TRIAL_USER_ID=<uuid> \
E2E_DISABLED_EMAIL=... E2E_DISABLED_PASSWORD=... E2E_DISABLED_USER_ID=<uuid> \
pnpm test:e2e --project=destructive-chromium
Your local AWS credentials (e.g. via aws sso login) must be allowed to invoke the stage's AccountStateTestHelper Lambda (lambda:InvokeFunction) and to write the stage's DynamoDB tables, which the org specs seed directly.
After a run, an HTML report is generated at playwright-report/. To view it:
pnpm exec playwright show-report
CI runs these tests automatically against preview deployments on PRs.
tests/e2e/destructive/auth.setup.ts puts each role's account into that role's state before logging it in. Trial periods can elapse and past_due subscriptions can advance to canceled between scheduled runs, so a previous run's state is not safe to reuse.
The setup calls setAccountState(userId, role) (tests/e2e/destructive/account-state.util.ts), which invokes the stage's AccountStateTestHelper Lambda (packages/backend/src/jobs/account-state-test-helper.ts). The Lambda writes the subscription status and dates to the account's org billing row, then sets the tenant status in every region the org is provisioned in:
| State | Billing row | Tenants |
|---|---|---|
paid | active, currentPeriodEnd = now + 30d | active |
unpaid | past_due, currentPeriodEnd = now + 30d, lastPaymentFailedAt = yesterday | active |
trial | trialing, trialEndsAt = now + 14d | active |
disabled | canceled | disabled |
paid_grace_period | grace_period, canceledAt = yesterday, gracePeriodEndsAt = yesterday + 30d | write-locked |
trial_grace_period | grace_period, trialEndsAt = yesterday, gracePeriodEndsAt = yesterday + 7d | write-locked |
No role rests in the two grace period states. They mirror the two ways production enters grace_period: a canceled subscription and a trial that ended without one. disabled disables the tenants before it writes canceled, in the order the grace-period enforcer uses. The Lambda returns the org id and each region's sync outcome. It does not sync with Stripe: the billing row is the source of truth for subscriptionStatus, and Stripe state is allowed to drift for these test users.
The Lambda can disable any account's tenants, so it is deployed to every stage except production, and the handler refuses to run when FILONE_STAGE is production.
Each role logs in once via tests/e2e/auth.setup.ts and the session cookies are persisted to .auth/<role>.json. Authenticated specs reuse that storage state instead of going through the Auth0 login UI on every test.
The .auth/ directory is gitignored — the JSON files are regenerated on every run and must not be committed.
ACCOUNT_STATES in packages/backend/src/jobs/account-state-test-helper.ts, and deploy it to the stages the suite runs against.STORAGE_STATE in tests/e2e/destructive/roles.util.ts.roles array in tests/e2e/destructive/auth.setup.ts (with email, password, userId).E2E_<ROLE>_EMAIL, E2E_<ROLE>_PASSWORD, E2E_<ROLE>_USER_ID to REQUIRED_CREDENTIAL_VARS in the same file.on.workflow_call.secrets and in the job's env: block, pass them under secrets: from both callers (.github/workflows/e2e-staging.yaml and .github/workflows/test-staging.yaml), and provision the corresponding GitHub secrets. actionlint reports a secret a caller forgot to pass.SEED_PLAN in tests/e2e/destructive/buckets.setup.ts, with needsActiveSubscription: true when its desired subscription state blocks writes.tests/e2e/destructive/tenant-disable.spec.ts checks that disabling an account cuts off the presigned URLs it already handed out (FIL-413). It runs on the disabled account, which rests in the disabled state: a canceled subscription and disabled tenants. Before its tests the spec switches the account to paid. In every region it then uploads an object, presigns a GET for it and checks that the URL serves the object. It switches the account to disabled afterwards. Each region gets two tests:
POST /api/presign for a GET must answer 403 SUBSCRIPTION_CANCELED.The account needs a one-time setup on each stage the suite runs against (staging only; the suite does not run against production, and the Lambda is not deployed there):
AccountStateTestHelper Lambda.GET /api/billing claims the trial, which writes the org's ORG#{orgId}/SUBSCRIPTION row in the BillingTable; the Lambda refuses an org without one./api/org/members on the stage's console (e.g. https://staging.fil.one/api/org/members). A new account is alone in its org, so the only member's userId is the account's FilOne user id. bin/e2e-register-policy-users.ts reads it the same way.E2E_DISABLED_EMAIL, E2E_DISABLED_PASSWORD and E2E_DISABLED_USER_ID. Do this before the spec lands on main: auth.setup.ts fails the whole run when any of them is missing, and the staging workflows run after every deploy..env.e2e.local.The first run seeds a bucket in every region (destructive-seed-buckets) and leaves the account disabled. A new account has no tenant in any region until its first bucket there, and a region still setting one up answers bucket creation with 503 "We are still setting up the region for you". If the first destructive-seed-buckets run fails that way, run it again.
tests/e2e/multi-org runs in its own multi-org-* Playwright projects on three accounts of its own: owner, member and fresh. Its credentials are E2E_ORG_{OWNER,MEMBER,FRESH}_{EMAIL,PASSWORD,AUTH0_ID}, where _AUTH0_ID is the account's Auth0 sub. In CI they come from GitHub secrets like the credentials above. Locally, put them in .env.e2e.local, which playwright.config.ts loads. The accounts need verified emails, because the setup (tests/e2e/multi-org/login.setup.ts) names the owner's and member's home orgs and claims their trials, and a trial is only granted to a verified email. Both setup projects grant every E2E account the organizations beta (its ALLOWLIST#{email} row), which creating orgs, Members and the audit log require. Each spec resets the rows it seeds, so runs repeat cleanly.
Against staging it runs as part of pnpm test:e2e. Against a local stack whose console runs on a port other than 5173, keep BASE_URL at https://localhost:5173, the only origin Auth0 accepts, and set E2E_ORIGIN_REWRITE to the real origin. The fixture then reroutes the browser's requests there:
eval "$(floci env)"; export LOCAL=true AWS_REGION=us-east-1
BASE_URL=https://localhost:5173 E2E_ORIGIN_REWRITE=https://localhost:5185 \
pnpm exec sst shell --stage local-org -- pnpm exec playwright test --project=multi-org-chromium --workers=1
Integration tests, located in tests/integration/, confirm that individual modules or services interact correctly with one another — for instance, ensuring Stripe webhook handlers produce the expected state transitions in DynamoDB — by running against real AWS and Stripe resources.
While E2E tests (Playwright) cover full, business-critical user journeys spanning the entire system, integration tests focus more narrowly on backend logic at specific integration points.
Run tests (requires deployed SST stage):
pnpm test:integration
Tests run inside sst shell so that SST resource bindings (table names, Stripe keys, etc.) are available as environment variables.
pnpm deploy:dev
Uses your OS username as the stage name. Serves the app at https://{username}.dev.fil.one (the SST stack creates the Route 53 record in the delegated dev.fil.one zone — see docs/architectural-decisions/2026-05-dev-subdomain.md). Stage names must be valid DNS labels: lowercase a-z, 0-9, -; 1–63 chars; no leading or trailing hyphen.
If you are having trouble deploying after SST changes (e.g., a version bump of SST or drift on components from manual actions), you may need to refresh the stack:
pnpm run refresh
pnpm deploy:dev
pnpm deploy:local deploys the app as the local stage into floci, an AWS emulator that runs in Docker. It uses no AWS account: sst.config.ts refuses a local deploy unless AWS_ENDPOINT_URL is set, and only accepts floci's account ID 000000000000.
The console runs on the Vite dev server at https://localhost:5173 and calls the stack's API Gateway directly. floci's CloudFront emulation serves the static site but cannot reach the API origin, and it does not run CloudFront Functions, so the deployed CloudFront URL is not usable for the app.
| Tool | Needed for |
|---|---|
| floci CLI, Docker | The emulator. |
| Node.js, pnpm | Building and deploying. |
| AWS CLI | Writing the Stripe webhook secret into floci's SSM. |
| Stripe CLI | Forwarding webhooks. Optional; billing needs it. |
A local stage talks to the same hosted services as a personal dev stack:
https://localhost:5173/api/auth/callback, which has to be registered in that tenant once (see docs/Auth0OneTimeSetup.md). Set Auth0ClientId, Auth0ClientSecret, Auth0MgmtRuntimeClientId and Auth0MgmtRuntimeClientSecret.StripeSecretKey, StripePublishableKey and StripePriceId from it, and create a billing meter with event name gb_month_meter there. Run stripe login against the same sandbox.The deploy skips SetupStack, so it registers nothing in Auth0 or Stripe and there is nothing to clean up in either when the stage is removed.
The first deploy gives every unset secret the value placeholder. Replace the ones you need from a shell pointed at floci:
eval "$(floci env)"
unset AWS_PROFILE
export LOCAL=true SST_STAGE=local
pnpm exec sst secret list # the header must read "# filone/local"
pnpm exec sst secret set StripeSecretKey # reads the value from stdin
Without SST_STAGE or --stage, SST falls back to the stage in .sst/stage, which is usually your personal dev stack.
floci start --persist ~/.floci/data
pnpm deploy:local
Start floci with --persist. Without it floci keeps everything in memory: a restart wipes the stage, its SST state and its secrets, and memory use grows with every deploy until floci runs out of heap and fails Lambda invocations with 500s.
The script checks the required tools, fills missing secrets with placeholders, builds, and deploys. When stripe login has been run, it stores the stripe listen signing secret in floci's SSM; the secret stays the same across stripe listen restarts. It ends by printing the API URL and two commands:
# Point the Vite proxy at the local API, then start the console
echo "DEV_PROXY_TARGET=http://<api-id>.execute-api.localhost.floci.io:4566" >> packages/website/.env.local
pnpm --filter @filone/website dev
# Forward Stripe webhooks while you work (the event list comes from WEBHOOK_EVENTS)
stripe listen --events <events> --forward-to http://<api-id>.execute-api.localhost.floci.io:4566/api/stripe/webhook
stripe listen forwards every matching event in the Stripe account. If other stages share the sandbox, their events reach your local stack too.
Every redeploy recreates the Lambda log subscription filters, because floci does not report them back the way AWS does. STAGE=<name> pnpm deploy:local deploys under another stage name.
Vite serves https://localhost:5173 with a self-signed certificate by default, which browsers warn about. A certificate from mkcert replaces it when present:
brew install mkcert
mkcert -install # adds a local CA to the system trust store
mkdir -p packages/website/.cert && cd packages/website/.cert && mkcert localhost
vite.config.ts picks up .cert/localhost.pem and .cert/localhost-key.pem on the next start. The directory is gitignored.
SMELT=true pnpm deploy:local points the Forge dev region (us-east-9) at a smelt network running on the same machine, so the console provisions tenants, keys and buckets in your local Hilt and Ingot. The other regions stay on their hosted services.
Smelt needs to run with INGOT_REGION=us-east-9, the region the console expects. The deploy then:
http://host.docker.internal:15110 and the S3 endpoint to http://host.docker.internal:15130 (SMELT_HOST overrides the host name), with presigned URLs on http://localhost:15130;ForgeDevManagementApiToken.The partner key replaces any hosted Forge dev token on the stage. To go back to the hosted network, set ForgeDevManagementApiToken again and deploy without SMELT.
floci's Lambda containers reach smelt through host.docker.internal. Presigned URLs, which the browser fetches directly, use http://localhost:15130 instead: an https:// page may fetch http://localhost but not other plain-HTTP hosts.
Ingot only accepts browser requests from origins in cors_allowed_origins (systems/ingot/config/config.yaml in smelt). Add https://localhost:5173 there and restart Ingot. To test Hilt changes, rebuild smelt's workspace binaries as its README describes; smelt runs whatever binary it last built.
eval "$(floci env)"
LOCAL=true pnpm exec sst remove --stage local
With --persist, the stage lives in ~/.floci/data and survives floci stop --remove. Deleting that directory resets the emulator.
Do not deploy to staging or production manually unless there is a very good reason. Use CI/CD.
For reference, the CI/CD pipeline runs:
pnpm run deploy:staging
pnpm run deploy:production
Custom domains require a pre-provisioned ACM certificate in us-east-1 and a DNS CNAME pointing to the CloudFront distribution (managed by a separate pipeline).
Infrastructure-only deploys are available for cases where only the base infra (OIDC, IAM roles) needs updating:
pnpm run deploy:infra:staging
pnpm run deploy:infra:production
pnpx sst dev
Runs Lambda functions locally with live reload. Changes to handler code take effect immediately without redeploying.
The deploy-time setup Lambda (setup-integrations) runs as a CloudFormation custom resource. If it fails (e.g., missing Auth0 scopes, API errors), the CloudFormation stack enters UPDATE_ROLLBACK_FAILED state and blocks all future deploys.
To unblock:
aws cloudformation continue-update-rollback \
--stack-name <stack-name> \
--region us-east-2 \
--resources-to-skip Setup
The stack name is in the error output (e.g., SetupStack-efd549a).
UPDATE_ROLLBACK_COMPLETE:aws cloudformation describe-stacks \
--stack-name <stack-name> \
--region us-east-2 \
--query 'Stacks[0].StackStatus'
For MFA-specific troubleshooting, see docs/architectural-decisions/2026-03-mfa-enrollment.md.
Custom domains require an ACM certificate in us-east-1 (CloudFront requirement). Managed in fil-one/infrastructure via HCP Terraform.
app.fil.one / staging.fil.one — one ACM cert per domain, DNS-validated through Cloudflare.*.dev.fil.one — single wildcard cert shared by every ephemeral stage (PR previews and personal dev stacks). dev.fil.one is delegated from Cloudflare to a Route 53 hosted zone in the staging AWS account, so SST creates per-stage A/AAAA records without needing a Cloudflare API token. See docs/architectural-decisions/2026-05-dev-subdomain.md.Auth0 powers authentication: Universal Login, two M2M applications per tenant (deploy-time and runtime), MFA, and passkeys as primary authentication. Most of this is automated by the deploy-time setup Lambda — but a handful of dashboard toggles must be configured manually once per tenant before the first deploy.
The full operator runbook (tenant settings, application settings, MFA, passkeys, and both M2M apps) is in docs/Auth0OneTimeSetup.md. For the design rationale, see the ADRs under docs/architectural-decisions/:
2026-03-mfa-enrollment.md — MFA factor selection + Post-Login Action2026-05-passkey-primary-authentication.md — passkeys as primary authenticationAuth0 credentials are managed as SST secrets (Auth0ClientId, Auth0ClientSecret). See the "Set SST secrets" step above.
Callback and logout URLs are configured automatically during deploy — no manual Dashboard edits needed. The deploy-time setup Lambda adds the correct URLs for the deployed domain.
Application settings (Applications > your app > Settings):
API setup (APIs > Create API):
app.fil.one (prod) — this must match AUTH0_AUDIENCE in sst.config.ts. It's what makes Auth0 issue a JWT access token (instead of an opaque one) and is the aud claim the middleware validates.MFA is opt-in per user (database and social connections). Auth0 handles enrollment and challenge via Universal Login. See docs/architectural-decisions/2026-03-mfa-enrollment.md for the full architectural decision record.
Two separate M2M applications are used to limit the scope of credentials exposed to Lambda functions.
Auth0MgmtClientId / Auth0MgmtClientSecret)Used only by the deploy-time setup Lambda to configure Auth0 on each deploy. Not available to runtime Lambda functions.
1. Enable MFA factors (Security > Multi-factor Auth) — manual, one-time per tenant:
2. Post-Login Action — automated on deploy:
The deploy-time setup Lambda (setup-integrations) automatically creates, deploys, and binds an MFA Enrollment Trigger Action to the Login flow (staging/production only). This Action checks app_metadata.mfa_enrolling on each login — when true, it triggers MFA enrollment via Universal Login and clears the flag after success. No manual Action setup is needed.
Two separate M2M applications per tenant limit the blast radius of credentials. The deploy-time app is only available to the setup Lambda; the runtime app is available to request-time handlers.
Auth0MgmtClientId / Auth0MgmtClientSecret)Used only by the deploy-time setup Lambda to configure Auth0 on each deploy.
| Environment | App name | Dashboard |
|---|---|---|
| Staging | SSTSetupM2MApp | Settings |
| Production | Deploy Automation | Settings |
Required scopes (Applications > M2M app > APIs > Auth0 Management API):
read:clients, update:clients, read:email_provider, create:email_provider, update:email_provider, create:actions, read:actions, update:actions, read:triggers, update:triggers
pnpx sst secret set Auth0MgmtClientId <M2M-client-id> [--stage <stage>]
pnpx sst secret set Auth0MgmtClientSecret <M2M-client-secret> [--stage <stage>]
Auth0MgmtRuntimeClientId / Auth0MgmtRuntimeClientSecret)Used by request-time Lambda handlers (update-profile, resend-verification, enroll-mfa, disable-mfa, delete-mfa-enrollment, get-me) to manage user records, trigger verification emails, and manage MFA enrollments.
| Environment | App name | Dashboard |
|---|---|---|
| Staging | Runtime M2M | Settings |
| Production | Runtime M2M | Settings |
Required scopes (Applications > M2M app > APIs > Auth0 Management API):
read:users, update:users, update:users_app_metadata, create:user_tickets, delete:users, delete:guardian_enrollments, read:authentication_methods, create:authentication_methods, delete:authentication_methods
pnpx sst secret set Auth0MgmtRuntimeClientId <M2M-client-id> [--stage <stage>]
pnpx sst secret set Auth0MgmtRuntimeClientSecret <M2M-client-secret> [--stage <stage>]
Use test mode first. Switch to live mode for production.
Fil.one StorageDecentralized cloud storage — $5.99/TB/month, $5.99/month minimumGB1000 GB, flat fee $5.99 — this flat fee is the monthly minimum$0.00599 per GB — the same $5.99/TB rate, for usage above the first tierprice_xxxxx)Usage is metered in decimal GB (GB_BYTES = 1e9, see the usage-reporting worker),
so 1000 GB of included usage is exactly the $5.99 the flat fee covers.
The first tier's flat amount is what the console shows as the monthly minimum:
GET /api/billing reports it as subscription.monthlyMinimumCents, read from the
price the org's subscription is actually billed on. Customers grandfathered on the
earlier $4.99 graduated price keep its $4.99 minimum, and those on the older plain
per-unit price have no tiers and therefore no minimum. The price
snapshot is cached on the billing record so the minimum stays correct while the
Stripe API is unavailable.
The endpoint never guesses the minimum: it fails the request instead of reporting "no minimum" when the price is graduated but carries no tiers, or when Stripe's exact decimal amount cannot be parsed.
GET /api/billing is a read model that grants nothing: it reports the stored
subscriptionStatus verbatim and never synthesizes entitlement. An account with
no billing record — or a record without a status (e.g. the customer mapping
create-setup-intent writes) — is reported as
{subscription: {planId: 'none', status: 'inactive'}}, the same answer the subscription guard
enforces with its 403 SUBSCRIPTION_INACTIVE. See
the ADR.
Settings > Billing > Customer portal — enable:
Webhook endpoints are created and managed automatically during deploy. The deploy-time setup Lambda creates the Stripe webhook endpoint with the correct URL for the deployed domain and stores the signing secret in AWS SSM Parameter Store. No manual configuration needed.
Events registered: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, customer.subscription.trial_will_end, invoice.payment_succeeded, invoice.payment_failed
Run this command to delete all webhooks created by PR preview deployments (including hooks from active pull requests):
stripe webhook_endpoints list --limit 100 | \
jq -r '.data[] | select(.metadata.stage // "" | startswith("pr-")) | .id' | \
xargs -I{} stripe webhook_endpoints delete {} --confirm
Stripe credentials are managed as SST secrets (StripeSecretKey, StripePriceId, StripePublishableKey). See the "Set SST secrets" step above.
Auth0 transactional emails (verification, password reset, etc.) are sent via SendGrid. The deploy-time setup Lambda configures Auth0 to use SendGrid automatically.
API keys are managed at: https://app.sendgrid.com/settings/api_keys
The SendGridApiKey SST secret should be a key with Mail Send permission only. See the "Set SST secrets" step above.
The project includes generated TypeScript clients for the Aurora APIs, built with Hey API:
packages/aurora-backoffice-client/ (tenant management, admin operations)packages/aurora-portal-client/ (access keys, buckets, tenant-facing operations)The backend uses an API token to authenticate with the Aurora Back Office API (e.g., to create tenants on user registration).
Generating the token:
Setting the SST secret:
pnpx sst secret set AuroraBackofficeToken <token> [--stage <stage>]
After API changes, update the relevant Swagger spec and regenerate:
Back Office client:
packages/aurora-backoffice-client/aurora-backoffice.swagger.json
with the downloaded filePortal client:
Download the updated Swagger spec from the Aurora Portal API docs and save it to packages/aurora-portal-client/aurora-portal.swagger.json:
curl https://docs.aur.lu/portal-api-spec.json -o packages/aurora-portal-client/aurora-portal.swagger.json && oxfmt
Reformat the file:
pnpm lint:fix
Regenerate both clients:
pnpm generate:api-clients
packages/website/src/components/)UI components live directly in the website package under packages/website/src/components/. There is no separate design-system package or git submodule.
The visual design draws on @filecoin-foundation/ui-filecoin as the original inspiration, but all components have been adapted for this Vite/React app and are maintained in-tree.
Telemetry is sent to Grafana Cloud. See docs/architectural-decisions/2026-03-observability-architecture.md for details.
Logs: CloudWatch Logs → Kinesis Firehose → Grafana Cloud Loki (per-stage, managed by the main stack).
Metrics: CloudWatch Metrics → Metric Stream → Kinesis Firehose → Grafana Cloud Prometheus (per-account, managed by the infra/ stack — one stream captures all Lambda metrics in the account regardless of stage). Developer stacks do not stream metrics to Grafana; use the CloudWatch console instead.
Generate API keys in Grafana Cloud (grafana.com → your stack → Connections → API keys):
<instanceId>:<apiKey> where instanceId is your Loki instance ID (sent as-is in the Firehose X-Amz-Firehose-Access-Key header)<instanceId>:<apiKey> where instanceId is your Prometheus instance ID# Main stack secrets
pnpx sst secret set GrafanaLokiAuth '<instanceId>:<apiKey>' [--stage <stage>]
# Infra stack secrets (run from infra/ directory)
cd infra && pnpx sst secret set GrafanaPrometheusAuth '<instanceId>:<apiKey>' --stage <stage>