Skip to content
archmax.ai

AWS (ECS Fargate)

This page sets up archmax automation on AWS: the image as an ECS Fargate service behind an Application Load Balancer (ALB), people signing in through the load balancer and Amazon Cognito, credentials kept in AWS Secrets Manager, the design agent on Amazon Bedrock and S3 through the task’s IAM role, and Redis either on Amazon MemoryDB or inside the container. What every deployment needs regardless of where it runs is in installation and operating a deployment.

Piece On AWS
The image (nginx, the API, the dispatcher) One ECS Fargate service, container port 8080
Workers In those tasks, or on a worker-host service of their own, with no task role (see Worker hosts)
Sign-in The ALB’s authenticate-oidc action against a Cognito user pool (AWS ALB sign-in), or the web app itself against Cognito or Auth0 (OIDC sign-in)
Workspace storage One S3 bucket, reached with the task role
The design agent’s model Bedrock, reached with the task role
Redis MemoryDB, or the container’s own Redis when REDIS_URL is unset
MongoDB Any MongoDB the tasks can reach, such as MongoDB Atlas over VPC peering or PrivateLink
Credentials’ secrets Secrets Manager, one secret per credential under archmax/credentials/, reached with the task role
The deployment’s own secrets Secrets Manager or SSM Parameter Store, injected when a task starts
  • AWS ALB sign-in (ARCHMAX_SIGN_IN=aws-alb). The load balancer signs people in at Cognito before any request reaches the platform, and forwards who they are in the header it signs, x-amzn-oidc-data. The platform verifies that header with the load balancer’s public key and checks that your load balancer signed it. The web app holds no token. This is the setup the rest of this page describes.
  • OIDC sign-in (ARCHMAX_SIGN_IN=oidc). The web app signs in at Cognito or Auth0 itself and sends its access token; the load balancer only forwards. See OIDC sign-in on AWS at the end of this page.

Every credential an owner stores — an API key, an OAuth token, an endpoint token’s digest — has secret fields, and ARCHMAX_SECRET_STORE names where the deployment keeps them:

  • AWS Secrets Manager (ARCHMAX_SECRET_STORE=aws-secrets-manager, recommended on AWS). Each credential’s secret fields are one secret, named <prefix><credential id>; MongoDB keeps only the secret’s name and the version the platform wrote. There is no master key to generate or keep. Every read and write shows in CloudTrail, the secrets are encrypted with the account’s aws/secretsmanager key or a key of your own, and the task role reaches only the prefix.
  • The database (db, or unset). The secret fields are sealed with AES-256-GCM under MASTER_KEY on the credential’s record in MongoDB, as on any other host.

The AWS store’s variables:

Variable Required What it is
ARCHMAX_AWS_SECRETS_MANAGER_REGION yes The region the secrets live in, such as eu-west-1.
ARCHMAX_AWS_SECRETS_MANAGER_PREFIX no What every secret’s name begins with; archmax/credentials/ by default. It ends in /.
ARCHMAX_AWS_SECRETS_MANAGER_KMS_KEY_ID no A customer-managed KMS key (id, ARN or alias) new secrets are encrypted with.
  • One prefix per deployment, chosen once. The platform creates, reads, writes and deletes only secrets under its prefix, and never lists any, so two deployments in one account need two prefixes that don’t nest (archmax/prod/credentials/ and archmax/staging/credentials/). The default stays clear of archmax/master-key and archmax/connections below, which the execution role reads and the task role must not.
  • A deleted credential’s secret is scheduled for deletion with a seven-day recovery window, and so is every secret of a deleted environment or agent. A live credential’s secret is never deleted: each change writes a new version of it.
  • Secrets Manager bills per secret and per 10,000 calls. The platform keeps a value it read in memory for up to five minutes, so a credential costs about one call per five minutes per task while it is in use.

Create a user pool and, in it, an app client for the load balancer:

  • The issuer. Use the pool’s classic issuer, https://cognito-idp.<region>.amazonaws.com/<pool-id>. The newer issuer-cognito-idp form doesn’t work with ALB authentication.
  • The app client has a client secret and the authorization code grant, with the scopes openid email profile. Its callback URL is https://<host>/oauth2/idpresponse (the load balancer’s own) and its sign-out URL is https://<host>/signed-out, the page the web app shows after signing out without a session.
  • Readable attributes. The load balancer signs Cognito’s userinfo answer, which carries the attributes the app client may read: allow email, email_verified, name and custom:managed_domain.
  • A short access-token lifetime, such as 15 minutes. Signing out expires the load balancer’s session cookies, but a cookie someone copied stays usable until the access token inside it expires.
  • Self sign-up off, unless anyone may create an account. Invitations and the waitlist decide who joins an organization, but a pool with self sign-up lets anyone sign in.

The platform admits only a person whose email the identity provider says is verified, because invitations, taking over an account by email and auto-onboarding all trust that address. Cognito’s own users verify theirs at sign-up. Federated users carry email_verified only when the federation maps it:

  • Google: map Google’s email_verified to email_verified.
  • Microsoft Entra ID: Entra sends no email_verified, and its email is an attribute a directory’s administrator can set to any address. Don’t map a blanket “verified”: any Entra directory could then claim any address. Restrict the federation to your own directories (each directory’s own issuer, https://login.microsoftonline.com/<directory-id>/v2.0), turn self sign-up off, and set ARCHMAX_EMAIL_VERIFICATION=off, which vouches for every address the identity provider supplies. That is safe only while the pool admits no address you don’t control. Otherwise, sign Entra users in through OIDC sign-in with Auth0, whose post-login Action checks that the email equals the user’s principal name.

A person the platform refuses sees a screen asking them to verify their address, so a missing mapping shows up at the first sign-in.

Auto-onboarding offers a newcomer membership when the identity provider asserts which domain manages their account. Cognito exposes custom attributes as custom:<name>, so add a custom attribute managed_domain, fill it from the federation’s attribute mapping (Google’s hd, the domain of a Google Workspace account), and tell the platform its name:

Terminal window
ARCHMAX_MANAGED_DOMAIN_CLAIM=custom:managed_domain

Make the attribute readable but not writable by the app client, so nobody can set their own.

An internet-facing ALB serving HTTPS on port 443 (an ACM certificate), and redirecting HTTP on port 80 to it. Set its idle timeout to 300 s: a binary AG-UI stream and an MCP tool call without progress can stay silent longer than the default 60 s, and nginx in the image cuts at 300 s anyway.

The rules for HTTPS, most specific first. “authenticate” is an authenticate-oidc action against the user pool’s app client, followed by a forward to the target group:

Path Action On an unauthenticated request Why
/api/webhooks/*, /api/callbacks/* forward — Called server to server; no person signs in
/api/health, /api/sign-in-settings, /api/branding, /api/sign-out forward — Public by design: the signed-out page wears the deployment’s branding, and signing out must work after the session expired
/signed-out, /assets/*, /fonts/*, /favicon*, /archmax-* forward — The page signing out returns to, and the static files it needs; none of them secret
/api/oauth2/* forward — A connection’s provider sends the browser back here, including an administrator granting consent who never signed in to the platform (the callback checks its own state), and an authorization server fetches the platform’s client metadata document here
/api/agui/*, /api/mcp/* authenticate allow Exposed endpoints admit callers who never signed in; a member’s session still identifies them
/api/* authenticate deny A request from the web app gets a 401 it can act on, not a redirect it can’t follow
default authenticate authenticate The web app itself

deny answers 401 when a request carries no session, but a request whose session expired is still redirected to Cognito. The web app sends its requests without following redirects and treats either answer as a lost session: it reloads the page once, so the load balancer signs the person in again, and shows a “sign-in failed” screen if the reload doesn’t help.

  • Target group: target type ip, HTTP on port 8080, health check path /healthz expecting 200. /healthz reports readiness: that the API answers and, when the container runs its own Redis, that this container’s dispatcher holds the single-dispatcher lease.
  • Deregistration delay: 60 s with MemoryDB, 15 s with the container’s own Redis (see Redis).
  • Security groups: the load balancer’s admits 443 (and 80) from the clients you serve; the tasks’ admits 8080 from the load balancer’s security group only. The platform verifies every header the load balancer signs, so a request that bypassed it carries no identity, but nothing else should reach the tasks.

Most of the platform is for signed-in people, but some routes are called by services and clients that never sign in. Each of them authenticates its caller itself — the secret token in its path, the endpoint’s incoming connection, a webhook’s signature, the OAuth2 flow’s state — which is why the load balancer’s rules above let them past its sign-in. If the deployment is reachable only from inside your network, through an internal load balancer, an IP restriction, a security group or AWS WAF, open exactly these to whoever must call them:

Route Called by Needed for Must be reachable from
POST /api/webhooks/<token> The sending system’s servers: any service that posts to a webhook trigger Webhook triggers The sender’s network; usually the internet
/api/callbacks/<token> (GET, HEAD, POST, PUT, PATCH, DELETE) The service of a piece’s callback trigger, which the platform registers there when the trigger activates (some services first check the address with a GET) Callback triggers The service’s servers; usually the internet
GET /api/oauth2/callback The browser of the person connecting an OAuth2 connection, or of the administrator granting consent, coming back from the provider OAuth2 connections Wherever those people’s browsers are — not the provider’s servers
GET /api/oauth2/client-metadata The authorization server of a Remote MCP Server connection authenticated with OAuth, reading who the platform is as a client OAuth connections to Remote MCP Servers That authorization server’s servers; usually the internet
/api/agui/<token> (POST, OPTIONS) AG-UI clients: the chat widget in the browsers of a site’s visitors, or another application Exposed AG-UI endpoints Wherever those callers are
/api/mcp/<token>, /api/mcp/<token>/.well-known/oauth-protected-resource MCP clients: assistants, IDEs, other agents Exposed MCP endpoints Wherever those clients run
  • APP_BASE_URL must be the address these callers use. The platform registers callback addresses with it and names an MCP endpoint’s address by it, so behind a split-horizon setup it is the outside name, not the internal one. The web app shows webhook and endpoint addresses on the origin it is served from.
  • Senders’ IP ranges. Behind an IP restriction, open the webhook and callback paths to the ranges the senders publish (GitHub’s hooks ranges in its meta API, Stripe’s webhook addresses, and so on), rather than to everyone.
  • AWS WAF. The common rule set’s body size rule blocks bodies over 8 KB, and webhook and callback bodies may be up to 256 KiB. Scope that rule down, or exclude these two paths from it.
  • An air-gapped deployment works without any of these routes: webhook and callback triggers, and endpoints called from outside, are then simply unavailable. Schedule and polling triggers still run, since the platform calls out for them (if it may call out at all, below).

A deployment that isn’t air-gapped reaches these from the tasks, through a NAT gateway or an egress proxy, or through VPC endpoints where AWS offers them:

Destination Why Route it through
public-keys.auth.elb.<region>.amazonaws.com The keys the load balancer signs x-amzn-oidc-data with; AWS ALB sign-in refuses everyone without it NAT or an egress proxy (a public AWS endpoint)
bedrock-runtime.<region>.amazonaws.com The design agent on Bedrock A Bedrock interface endpoint, or NAT
S3 Workspace storage An S3 gateway endpoint
MongoDB, MemoryDB Data and queues Inside the VPC, peering or PrivateLink
Secrets Manager, SSM, ECR, CloudWatch Logs Starting the task (its secrets, its image and its log) and, with the AWS store, the credentials’ secrets while it runs Interface endpoints (ECR also needs the S3 gateway endpoint), or NAT
Your identity provider’s issuer OIDC sign-in only: its discovery document, keys and userinfo NAT
What owners configure (LLM providers, MCP servers, the services connections reach, OAuth2 providers’ token endpoints) and the config’s skill registries (git over HTTPS) Running agents and their tools NAT or an egress proxy

The platform refuses outbound requests to private addresses, so every service an owner configures, and the identity provider, must be reachable at a public address.

What the platform itself calls with the task’s credentials: S3, the credentials’ secrets in Secrets Manager, and, for the design agent, Bedrock. Grant Bedrock on the inference profile and on the foundation model in every region the profile routes to, since an eu.* profile spans several EU regions:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": [
"arn:aws:bedrock:eu-west-1:<account>:inference-profile/eu.anthropic.<model-id>",
"arn:aws:bedrock:*::foundation-model/anthropic.<model-id>"
]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::<bucket>/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::<bucket>"
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:CreateSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:DeleteSecret"
],
"Resource": "arn:aws:secretsmanager:eu-west-1:<account>:secret:archmax/credentials/*"
}
]
}

The Secrets Manager statement is the whole of what the AWS store needs: no ListSecrets, and nothing outside the prefix. Leave it out with the database store. With ARCHMAX_AWS_SECRETS_MANAGER_KMS_KEY_ID set, also grant kms:GenerateDataKey and kms:Decrypt on that key; the account’s aws/secretsmanager key needs no grant.

Name the model in the config by its system inference profile ID (eu.anthropic.…), not by an application inference profile ARN: prompt caching applies only to the former.

designAgent:
provider: bedrock
region: eu-west-1
model: eu.anthropic.claude-sonnet-5-5
# The model's Bedrock prices in USD per million tokens, required with the section.
# Illustrative: enter those of the profile you name (`eu.`, `us.` and `global.` are
# priced separately), from Bedrock's current price list.
prices:
input: 2.20
output: 11.00
cacheRead: 0.22
cacheWrite: 2.75

The account must have access to the model: Anthropic’s models on Bedrock ask for a one-time use case form in the Bedrock console before the first call, and until then every design-agent turn fails with an access error.

A bucket encrypted with a customer-managed KMS key also needs kms:GenerateDataKey and kms:Decrypt on that key. With no S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY, S3 is signed with the task role too. Owner connections to Bedrock never use the task role; they carry their own keys.

What ECS needs to start the task: the managed AmazonECSTaskExecutionRolePolicy (the image and the log), plus secretsmanager:GetSecretValue on the secrets and ssm:GetParameters on the parameters the task definition names, and kms:Decrypt for a customer-managed key.

Every release is published to GitHub Container Registry, ghcr.io/archmax-ai/automation, for linux/amd64 and linux/arm64, so a task runs on either Fargate architecture (runtimePlatform.cpuArchitecture X86_64 or ARM64). The package is private until an owner makes it public, so ECS pulls it one of two ways:

  • Copied into ECR (recommended). Copy each release into a repository of your account, after docker login to both registries: docker buildx imagetools create --tag <account>.dkr.ecr.<region>.amazonaws.com/archmax:<X.Y.Z> ghcr.io/archmax-ai/automation:<X.Y.Z>. The tasks then pull from your own account, through ECR’s interface endpoints when they have no NAT, and the execution role’s managed policy already allows it.
  • From GHCR directly. Keep a GitHub token that can read packages in Secrets Manager as {"username": "<github-user>", "password": "<token>"}, name that secret in the container definition’s repositoryCredentials.credentialsParameter, and allow the execution role secretsmanager:GetSecretValue on it. The tasks need outbound access to ghcr.io.

Name a release by its tag (1.5.0), or by its digest to pin it exactly.

Secrets come from Secrets Manager (a whole secret, or one key of a JSON secret) or from SSM Parameter Store, read once when a task starts, so a rotated value needs a new deployment. environmentFiles loads a .env file from S3 if you prefer one file:

{
"family": "archmax",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"taskRoleArn": "arn:aws:iam::<account>:role/archmax-task",
"executionRoleArn": "arn:aws:iam::<account>:role/archmax-execution",
"containerDefinitions": [
{
"name": "archmax",
"image": "ghcr.io/archmax-ai/automation:<X.Y.Z>",
"portMappings": [{ "containerPort": 8080 }],
"linuxParameters": { "capabilities": { "drop": ["ALL"] } },
"stopTimeout": 120,
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "APP_BASE_URL", "value": "https://<host>" },
{ "name": "OAUTH2_REDIRECT_URL", "value": "https://<host>/api/oauth2/callback" },
{ "name": "ARCHMAX_SIGN_IN", "value": "aws-alb" },
{ "name": "ARCHMAX_AWS_ALB_ARN", "value": "arn:aws:elasticloadbalancing:eu-west-1:<account>:loadbalancer/app/<name>/<id>" },
{ "name": "ARCHMAX_AWS_ALB_SIGN_OUT_URL", "value": "https://<domain>.auth.eu-west-1.amazoncognito.com/logout?client_id=<client-id>&logout_uri=https%3A%2F%2F<host>%2Fsigned-out" },
{ "name": "ARCHMAX_MANAGED_DOMAIN_CLAIM", "value": "custom:managed_domain" },
{ "name": "ARCHMAX_SECRET_STORE", "value": "aws-secrets-manager" },
{ "name": "ARCHMAX_AWS_SECRETS_MANAGER_REGION", "value": "eu-west-1" },
{ "name": "S3_BUCKET", "value": "<bucket>" },
{ "name": "S3_REGION", "value": "eu-west-1" },
{ "name": "ARCHMAX_TRUSTED_PROXIES", "value": "<the VPC's CIDR range>" }
],
"secrets": [
{ "name": "MONGODB_URI", "valueFrom": "arn:aws:secretsmanager:eu-west-1:<account>:secret:archmax/connections-<suffix>:MONGODB_URI::" },
{ "name": "REDIS_URL", "valueFrom": "arn:aws:secretsmanager:eu-west-1:<account>:secret:archmax/connections-<suffix>:REDIS_URL::" },
{ "name": "ARCHMAX_CONFIG", "valueFrom": "arn:aws:ssm:eu-west-1:<account>:parameter/archmax/config" }
],
"environmentFiles": [{ "value": "arn:aws:s3:::<config-bucket>/archmax.env", "type": "s3" }],
"logConfiguration": {
"logDriver": "awslogs",
"options": { "awslogs-group": "/ecs/archmax", "awslogs-region": "eu-west-1", "awslogs-stream-prefix": "archmax" }
}
}
]
}
  • stopTimeout: 120, Fargate’s maximum: on a stop the container drains, letting running turns finish within that time.
  • ARCHMAX_TRUSTED_PROXIES names the VPC’s range, which the load balancer’s addresses are in: the API’s per-caller rate limits then count each client, not the load balancer.
  • linuxParameters dropping ALL: the image needs nothing it removes, and with it no process the task starts, its health check included, can read another’s environment (see the image).
  • No MASTER_KEY with the AWS store: the deployment refuses to start with one set beside ARCHMAX_SECRET_STORE=aws-secrets-manager. With the database store, add { "name": "MASTER_KEY", "valueFrom": "arn:aws:secretsmanager:eu-west-1:<account>:secret:archmax/master-key-<suffix>" } to secrets and leave out ARCHMAX_SECRET_STORE and ARCHMAX_AWS_SECRETS_MANAGER_REGION.
  • ARCHMAX_CONFIG is one line of JSON (pnpm config:env produces it from a config.yaml). It fits a standard SSM parameter up to 4 KB, an advanced one up to 8 KB, or Secrets Manager up to 64 KB. Its ${{VAR}} placeholders pass through ECS untouched.
  • AWS AppConfig and AWS Config are not sources. AppConfig doesn’t inject environment variables (it needs its agent at runtime, which the platform doesn’t read), and AWS Config is a compliance inventory, not a configuration store.

Moving a running deployment’s credentials to Secrets Manager

Section titled “Moving a running deployment’s credentials to Secrets Manager”

A deployment that started with the database store moves its credentials’ secrets with a tool, run once with the image’s code against the deployment’s MongoDB:

  1. Add the Secrets Manager statement to the task role, and give the role you run the tool with the same permissions.

  2. Stop the service (desired count 0), so no task writes a credential while it moves.

  3. With the deployment’s variables (MONGODB_URI, MASTER_KEY, …) and the AWS store’s set beside them, see what would move, then move it:

    Terminal window
    ARCHMAX_AWS_SECRETS_MANAGER_REGION=eu-west-1 \
    pnpm --filter @archmax/core exec tsx tools/move-secrets.ts --to aws-secrets-manager
    ARCHMAX_AWS_SECRETS_MANAGER_REGION=eu-west-1 \
    pnpm --filter @archmax/core exec tsx tools/move-secrets.ts --to aws-secrets-manager --apply

    A credential reported as changed while moving is moved by running it again; a run that finds nothing left to move changes nothing.

  4. In the task definition, set ARCHMAX_SECRET_STORE=aws-secrets-manager and the region, remove MASTER_KEY, and start the service.

  5. Once the deployment works, delete the master key’s secret.

Moving back is the same with --to db and a MASTER_KEY set beside the AWS store’s variables. Do it before going back to a release older than the AWS store: such a release reads only the database.

Every turn and every call through a connection runs in a worker. By default the tasks above start them as processes of their own, which shares the task’s network and its task role’s endpoint with code the platform does not control. A deployment serving more than one organization runs them on a worker-host service instead, where neither reaches anything:

  • The service. The same image, task definition command ["worker-host"], container port 7340, and linuxParameters dropping ALL. No taskRoleArn: without a task role, a task has no credentials endpoint to call. The execution role reads its one secret.
  • Its one secret. ARCHMAX_WORKER_HOST_KEY (openssl rand -base64 32), in Secrets Manager, given to the worker hosts and to the platform’s tasks alike. The host refuses to start with anything else of the platform’s in its environment: no MONGODB_URI, no REDIS_URL, no S3_*, no ARCHMAX_CONFIG.
  • The platform’s tasks add ARCHMAX_WORKER_HOST (<name>:7340, through Service Connect, Cloud Map or an internal Network Load Balancer passing TCP through) and the key. Every connection is TLS, authenticated by the key both ways, so the load balancer needs no certificate.
  • Its security group admits 7340 from the platform tasks’ security group only. Its outbound rules reach the internet through the NAT gateway, for the model providers and the APIs the collections call, but nothing in the VPC: MemoryDB’s, MongoDB’s endpoint’s and the VPC endpoints’ security groups admit the platform tasks’ security group, never this one.
  • Capacity. Each host runs as many workers as its dispatchers send it. Scale the service on CPU and memory, and keep the platform tasks’ ARCHMAX_WORKER_TURN_SLOTS and ARCHMAX_WORKER_CALL_SLOTS within what the hosts hold together.

Rolling back to a release without worker hosts simply stops using them.

Choose one of two setups.

For more than one task, and deploys without downtime. MemoryDB keeps queued jobs, trigger schedules and in-flight state across node failures and deploys.

  • A MemoryDB cluster in the tasks’ VPC, with a security group that admits the tasks only.
  • An ACL user. Its URL, rediss://<user>:<password>@clustercfg.<name>.memorydb.<region>.amazonaws.com:6379, goes into Secrets Manager and reaches the task as REDIS_URL. The platform detects cluster mode from the server.
  • A parameter group with maxmemory-policy set to noeviction, which the job queue requires.
  • The service deploys as ECS’s rolling default, with any desiredCount, and a 60 s deregistration delay.

ElastiCache works with the same settings, with cluster mode on or off.

For one task, without a Redis to run: leave REDIS_URL out, and the container starts a Redis of its own that serves only its own API and dispatcher. Its state lives on the task’s ephemeral storage, so a task replacement loses what was only in Redis: trigger firings accepted but not yet run, OAuth2 connects in progress and live chat streams. Turns and evaluations it was running end as failed, as do environment operations queued or running; queued turns and evaluations and provisioning are enqueued again, and schedules are restored. See the image for the full list.

Only one such container may serve at a time, so set the service to stop the old task before it starts the new one:

Setting Value
desiredCount 1
deploymentConfiguration.minimumHealthyPercent 0
deploymentConfiguration.maximumPercent 100
Deregistration delay 15 s
Health check grace period at least 60 s

Each deploy has a short outage, from the old task’s deregistration until the new one passes its health check. A second task by mistake never turns healthy: it waits for the single-dispatcher lease the first one holds, and ECS keeps replacing it, which the service’s events show.

Without the load balancer’s sign-in, the web app signs people in itself, and every rule of the load balancer just forwards. Set ARCHMAX_SIGN_IN=oidc with the ARCHMAX_OIDC_* variables in the task definition, and leave out every ARCHMAX_AWS_ALB_* one: a variable of the mode not named refuses the start. Use a public app client (no client secret) with the authorization code grant and PKCE, and allow both https://<host> and https://<host>/silent-callback.html as callback URLs: the second is where the web app’s hidden check restores a session after a reload.

  • Cognito as the issuer needs ARCHMAX_OIDC_SIGN_OUT_URL: Cognito’s logout takes client_id and logout_uri rather than the standard end-session parameters, https://<domain>.auth.<region>.amazoncognito.com/logout?client_id=<client-id>&logout_uri=https%3A%2F%2F<host>. Its access tokens carry no audience, so leave ARCHMAX_OIDC_AUDIENCE unset; the platform then checks the client id instead.
  • Auth0 as the issuer needs ARCHMAX_OIDC_AUDIENCE, the identifier of the Auth0 API for the platform, or Auth0 issues access tokens nobody but Auth0 can read.

The environment variables reference lists both modes’ variables.