Skip to main content

Installation Guide

Deploy Align on your Kubernetes cluster.

Before you start: the checklist​

Gather these before the install session. Each line says who issues it. The two marked Align take us a day or more, so ask for them first. Send the AWS account ID and the identity provider details to sales@align.tech.

#You needWho issues itWhere it is used
1Registry host, region, chart version and release tagAlign, at onboardingGetting the Helm Chart
2Either the 12-digit AWS account ID that will pull our images (we grant that account pull access, and without it the first helm command returns a 401), or nothing, if you use the offline bundle, which needs no AWS account and no registry access.You give it to Align, or ask for the bundleAuthenticate to the registry, Offline install
3license.jwtAlignLicensing
4An OpenAI key. Natural-language search embeds with OpenAI, and without a key a question returns an error (see Step 9). A GPU model for chat does not replace it.You (the provider, or your platform team)Step 3, LLM Setup
5Kubernetes 1.25+, Helm 3.10+, PostgreSQL 15+ with pgvector, Redis 6+, an ingress controller, two DNS names and TLSYou (your platform team)Prerequisites
6Two public addresses: the API (gateway.apiBaseUrl) and the web app (gateway.frontendUrl). They go into every invite link and OAuth callback.You choose themStep 4
7An OpenID Connect app in your identity provider: issuer URL, client ID, client secretYou (your identity provider admin)Step 8
8The email of the person who becomes the first adminYou decideStep 8
9OAuth apps for each connector you enable (Slack, Jira, GitHub and so on). Your own apps, one per tool.Your admin registers each oneConnector Authentication
10A laptop with a terminal, and Node 22.16 or newer, for the last stepYouStep 9
11A named security contact: one person, with a work email. We record it at onboarding.You give it to Align (we record it)Security advisories

Items 7 and 9 need an administrator of the other system (your identity provider, Slack, the Atlassian site, the Microsoft tenant, the GitHub organisation). Have that person on the call, or have them approve beforehand.

Your first tenant and first admin are one chart setting. Step 8 shows it: selfHost.bootstrap creates your tenant, makes your first admin, and removes Align's own seed data. It is off until you turn it on.

Commands on this page assume the Helm release is named align, which gives secrets such as align-database and align-resend. If you chose another release name, the chart prefixes the secrets it creates with that name instead.

Security advisories​

When a vulnerability in Align puts your install at risk, we tell you directly: by email to your named security contact, and in your shared Slack channel. Each advisory lists the affected versions, the fixed version, and whether you need to act now or can wait for your next routine upgrade. When an upstream fix exists, we ship it for a critical finding within 7 days and a high one within 30, as a new signed release. Give us the contact (item 11) when you start, and tell us if the person changes.

Getting the Helm Chart​

Align's chart and images are in a private AWS ECR registry. We give you the registry host at onboarding and grant your AWS account pull access.

These pages write $ALIGN_REGISTRY for that host. Set it once and the examples below work as written:

export ALIGN_REGISTRY="<the registry host issued to you at onboarding>"
export ALIGN_REGISTRY_REGION="<the region issued to you at onboarding>"
export ALIGN_CHART_VERSION="<the chart version for your release>"
export ALIGN_VERSION="<the release tag for your release, e.g. v0.22.26>"

Authenticate to the registry​

Do this first. Every helm and docker command on this page pulls from a private registry, and without credentials the very first one fails with a 401 rather than anything that names the cause.

The registry is AWS ECR, and onboarding grants your AWS account pull access to it. One token serves both clients:

# Helm, for the chart
aws ecr get-login-password --region "$ALIGN_REGISTRY_REGION" | \
helm registry login "$ALIGN_REGISTRY" --username AWS --password-stdin

# Docker, for the images - only needed if you are mirroring (Option B below)
aws ecr get-login-password --region "$ALIGN_REGISTRY_REGION" | \
docker login "$ALIGN_REGISTRY" --username AWS --password-stdin
note

These are your credentials, so that your machine can pull. They are a different thing from the imagePullSecrets created further down this page, which are what let the cluster pull images at runtime. You need both, and neither substitutes for the other.

ECR tokens are short-lived, so re-run the login if a pull starts returning 401 partway through an upgrade.

Let the cluster pull the images​

Logging in lets your laptop pull. The cluster pulls on its own, and it needs its own access:

  • EKS in the AWS account you gave us. The node role pulls the images. You need nothing else here.

  • Any other cluster. Create a pull secret from an ECR token, once the align namespace exists (Step 1):

    kubectl create secret docker-registry registry-credentials \
    --namespace align \
    --docker-server="$ALIGN_REGISTRY" \
    --docker-username=AWS \
    --docker-password="$(aws ecr get-login-password --region "$ALIGN_REGISTRY_REGION")"

    Step 4 refers to it as global.imagePullSecrets. The token expires after 12 hours, so a pod that restarts a day later cannot pull. For anything beyond a trial, mirror the images into a registry your cluster already trusts (Option B).

Your onboarding pack names both versions above. To see what the registry actually holds:

helm show chart oci://$ALIGN_REGISTRY/align/charts/align # omit --version to take the latest
# Pull the chart
helm pull oci://$ALIGN_REGISTRY/align/charts/align --version "$ALIGN_CHART_VERSION"

# Or install directly
helm install align \
oci://$ALIGN_REGISTRY/align/charts/align \
--version "$ALIGN_CHART_VERSION" \
--namespace align \
--values values.yaml
note

For production deployments, we recommend mirroring artifacts to your own registry (Option B below).

For production deployments, we recommend mirroring all artifacts to your own registry. This provides:

  • Reliability - No dependency on Align's infrastructure
  • Security - Full control over what runs in your cluster
  • Compliance - Artifacts scanned by your security tools
  • Registry mirroring - Mirror the images to a registry inside your network

Step 1: Mirror the Helm Chart​

# Pull the chart from Align's registry
helm pull oci://$ALIGN_REGISTRY/align/charts/align --version "$ALIGN_CHART_VERSION"

# Push to your registry (choose your cloud provider)
helm push "align-${ALIGN_CHART_VERSION}.tgz" oci://your-registry/align/charts

Step 2: Mirror Docker Images​

All images for a release use the same version tag:

VERSION="$ALIGN_VERSION"
SOURCE_REGISTRY="$ALIGN_REGISTRY/align"

# Set your target registry based on your cloud:
# AWS ECR: TARGET_REGISTRY="123456789.dkr.ecr.us-east-1.amazonaws.com/align"
# Azure ACR: TARGET_REGISTRY="myregistry.azurecr.io/align"
# GCP: TARGET_REGISTRY="us-docker.pkg.dev/my-project/align"
# Docker Hub: TARGET_REGISTRY="docker.io/myorg/align"
TARGET_REGISTRY="your-registry/align"

# The 17 images every release signs. Mirror all of them: the chart turns on six connectors
# by default, including connector-azure-devops and connector-m365-documents.
IMAGES=(
"gateway"
"brain"
"ui"
"migrations"
"connector-slack"
"connector-github"
"connector-jira"
"connector-confluence"
"connector-zoom"
"connector-teams"
"connector-align"
"connector-linear"
"connector-notion"
"connector-azure-devops"
"connector-m365-documents"
"connector-google-drive"
"connector-google-meet"
)

for image in "${IMAGES[@]}"; do
docker pull "${SOURCE_REGISTRY}/${image}:${VERSION}"
docker tag "${SOURCE_REGISTRY}/${image}:${VERSION}" "${TARGET_REGISTRY}/${image}:${VERSION}"
docker push "${TARGET_REGISTRY}/${image}:${VERSION}"
done

Step 3: Configure Helm to Use Your Registry​

# values.yaml
global:
# Your container registry ROOT - not including the `align/` path segment, which the
# chart already carries in each image's repository. The mirror step above pushes to
# ${TARGET_REGISTRY}/<image>, so `your-registry` here resolves to
# `your-registry/align/gateway`, which is exactly where the mirror put it.
# AWS ECR: imageRegistry: "123456789.dkr.ecr.us-east-1.amazonaws.com"
# Azure ACR: imageRegistry: "myregistry.azurecr.io"
# GCP: imageRegistry: "us-docker.pkg.dev/my-project"
imageRegistry: your-registry

# Registry authentication (if required)
imagePullSecrets:
- name: registry-credentials

Creating Registry Pull Secrets​

AWS ECR
# Create secret with ECR credentials
kubectl create secret docker-registry ecr-credentials \
--namespace align \
--docker-server=123456789.dkr.ecr.us-east-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password --region us-east-1)

Or use ECR credential helper with IRSA.

Azure ACR
# Create secret with ACR credentials
kubectl create secret docker-registry acr-credentials \
--namespace align \
--docker-server=myregistry.azurecr.io \
--docker-username=<service-principal-id> \
--docker-password=<service-principal-password>

Or use ACR with AKS managed identity.

Google Artifact Registry
# Create secret with GCP credentials
kubectl create secret docker-registry gcr-credentials \
--namespace align \
--docker-server=us-docker.pkg.dev \
--docker-username=_json_key \
--docker-password="$(cat service-account-key.json)"

Or use Workload Identity.

Registry-based distribution

Align is distributed as signed container images and a Helm chart from Align's private registry - source code is never distributed. The license is a JWT file validated locally. By default the gateway also checks in once a day to renew it (an offline license makes no call). See Licensing for details.

Prerequisites​

Before you begin, ensure you have:

  • Kubernetes 1.25+ cluster (EKS, AKS, GKE, or self-managed)
  • Helm 3.10+
  • kubectl configured for your cluster
  • PostgreSQL 15+ database with pgvector extension:
    • AWS: RDS PostgreSQL or Aurora PostgreSQL
    • Azure: Azure Database for PostgreSQL Flexible Server
    • GCP: Cloud SQL for PostgreSQL
    • Self-hosted: PostgreSQL with pgvector installed
  • Ingress controller (Traefik, nginx, or cloud-native like ALB/Application Gateway)
  • (Recommended) cert-manager for TLS

Step 1: Create Namespace​

kubectl create namespace align

Step 2: Configure Database​

By default the chart does not run a database, so you need a PostgreSQL 15+ instance with the pgvector extension before you install (see Prerequisites above for managed-service options).

Fast path for a pilot: let the chart bring its own Postgres, Redis and secrets​

Three opt-in switches replace most of Steps 2, 3 and the RLS section of Step 7. All are off by default, so an existing install is not changed.

# values.yaml
postgresql:
enabled: true # single-replica pgvector StatefulSet, plus the align-database Secret
redis:
enabled: true # password-protected Redis, wired into the gateway as REDIS_URL
rlsCutover:
enabled: true # sets the align_app password and verifies it, before the gateway starts
secrets:
create: true # generates the JWT, cookie, service and master encryption keys
cognito:
generatePlaceholder: true # the Cognito placeholder from Step 3 (only needed without OIDC)

With those on, skip the rest of this step, the internal-secrets and Cognito parts of Step 3, the Redis Secret, and the RLS section of Step 7. You still create align-llm and, for a license install, align-license.

What you accept for it:

  • The bundled database is for pilots. One replica, no backup, no failover. Use a managed database for anything you cannot lose.
  • It is created once. The Postgres objects are install-time hooks, so helm upgrade does not change them and helm uninstall leaves them, and the data volume, in place. Delete them by hand to start clean.
  • postgresql.enabled needs rlsCutover.enabled. The chart refuses to render one without the other.
  • Helm CLI only. The generated passwords and keys are read back from the cluster so they stay the same on upgrade. ArgoCD, Flux and helm template cannot read them back, so under GitOps set postgresql.auth.password, postgresql.auth.appPassword and secrets.internal.masterEncryptionKey yourself.
  • Turn it on at the first install. Enabling postgresql on an existing release does nothing, because install hooks do not run on upgrade. The chart also refuses to overwrite an existing align-database Secret it did not create.
  • Back up master-encryption-key. It encrypts stored connector tokens. The chart keeps the generated value across upgrades, but a deleted Secret takes it with it.

Without the fast path, create a secret with your database credentials:

kubectl create secret generic align-database \
--namespace align \
--from-literal=url="postgresql://user:password@host:5432/align" \
--from-literal=host="your-db-host.example.com" \
--from-literal=port="5432" \
--from-literal=username="align" \
--from-literal=password="your-secure-password"

This url key is the database OWNER connection - it is what migrations use to create roles, tables and policies, and it is the only thing that can exist before Step 7 runs. Once migrations have run, come back and add the restricted align_app connection so the gateway runs with PostgreSQL Row-Level Security enforced instead of connecting as the table owner - see "Enable the RLS Backstop" under Step 7 below.

Step 3: Configure Secrets​

Align requires several secrets for operation. See Secrets Management for detailed options.

Minimal Secrets​

At minimum, create internal secrets:

kubectl create secret generic align-internal \
--namespace align \
--from-literal=jwt-secret="$(openssl rand -base64 32)" \
--from-literal=cookie-secret="$(openssl rand -base64 32)" \
--from-literal=encryption-key="$(openssl rand -base64 32)" \
--from-literal=master-encryption-key="$(openssl rand -base64 32)" \
--from-literal=service-auth-token="$(openssl rand -base64 32)" \
--from-literal=github-inbound-token="$(openssl rand -base64 32)"

master-encryption-key is a different key from encryption-key above - it is what encrypts OAuth tokens and connector credentials at rest, and the gateway refuses to boot without it. See Secrets Management for what each key does.

github-inbound-token is needed only if you enable the GitHub connector (it is on in the default values). With the key missing, the gateway and the GitHub connector pods stay in CreateContainerConfigError and helm install --wait times out. Leave it out and set connectors.github.enabled: false if you do not run that connector.

Cognito placeholder (only without OIDC)​

In production the gateway needs one working sign-in method to start. A usable OIDC config counts, so with OIDC set up in Step 8 you can skip this secret. Releases from before ALI-1755 still need it. Without a sign-in method both gateway pods crash with Cognito configuration missing: COGNITO_USER_POOL_ID and COGNITO_APP_CLIENT_ID are required in production mode. If you see that error, create the secret with placeholders:

kubectl create secret generic align-cognito \
--namespace align \
--from-literal=user-pool-id="us-east-1_PLACEHOLDER" \
--from-literal=client-id="placeholder"

LLM Secrets (Required for AI features)​

kubectl create secret generic align-llm \
--namespace align \
--from-literal=openai-api-key="sk-..." \
# OR
--from-literal=anthropic-api-key="sk-ant-..."

Or see LLM Setup for self-hosted models.

Redis Secret (Required for more than one gateway replica)​

gateway.replicaCount above 1 needs a shared session store - see the gateway.redis block in Step 4 below. Referencing it by secretName (recommended: a values file that lands in git should not carry a Redis URL, which can embed credentials) needs the secret created first:

kubectl create secret generic align-redis \
--namespace align \
--from-literal=url="redis://your-redis:6379"

Or set gateway.redis.url directly to an inline URL and skip this secret.

Step 4: Create Values File​

Create a values.yaml file for your deployment:

# values.yaml
global:
environment: production

# REQUIRED. The registry root your images come from - the Align registry host issued at
# onboarding, or your own registry once you have mirrored our images into it. The chart
# refuses to render if this is unset: an empty value would produce a bare image name, and
# Docker resolves a bare name against Docker Hub rather than anywhere Align controls.
imageRegistry: "<your ALIGN_REGISTRY, or your own registry root>"

# REQUIRED in practice. The release tag, the same value as ALIGN_VERSION. Left empty, every
# image resolves to `latest`, which is not the release your chart version belongs to.
imageTag: "<your ALIGN_VERSION, e.g. v0.22.26>"

# Only on a cluster that needs a pull secret (see "Let the cluster pull the images").
# Delete these two lines on EKS in the account you gave us.
imagePullSecrets:
- name: registry-credentials

# Database
database:
secretName: align-database

# Gateway
gateway:
replicaCount: 2
# The public address of the API, as people type it: https, no path. Org invite links
# carry it, and so does every connector's OAuth callback URL. The chart reads this value
# but does not declare it in its own values.yaml. Leave it out and the chart derives it
# from the first ingress host below, and renders no public address while that ingress is off.
apiBaseUrl: "https://api.yourdomain.com"
# REQUIRED above one replica: without a shared store each gateway pod keeps its own
# in-process cache, so a revoked session stays live on the pods that did not serve the
# revoke call. The chart refuses to render replicaCount > 1 without one. Any
# Redis-compatible service works (ElastiCache, Memorystore, Azure Cache, Dragonfly, KeyDB).
redis:
secretName: align-redis # or: url: "redis://your-redis:6379"
secretKey: url
frontendUrl: "https://app.yourdomain.com"
ingress:
enabled: true
className: "nginx" # or "traefik"
hosts:
- host: api.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.yourdomain.com

# UI
ui:
replicaCount: 2
ingress:
enabled: true
className: "nginx"
hosts:
- host: app.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: app-tls
hosts:
- app.yourdomain.com

# Brain (AI Service)
brain:
replicaCount: 2

# Connectors - disable unused ones
connectors:
slack:
enabled: true
teams:
enabled: true
jira:
enabled: true
github:
enabled: true
linear:
enabled: false # Enable if needed
align:
enabled: false # Enable for AI assistant integration (Claude, Cursor)

# Secrets
secrets:
create: false # Use pre-created secrets

See Configuration Reference for all options.

Step 4b: GPU Inference (Optional)​

To run LLM inference locally instead of using cloud APIs, add GPU configuration to your values file. This requires GPU nodes in your cluster.

# values.yaml (add to existing file)
gpu:
devicePlugin:
enabled: true
llmServer:
enabled: true

# Tell Brain which model the local server runs
brain:
extraEnv:
- name: LOCAL_LLM_MODEL
value: "meta-llama/Llama-3.1-8B-Instruct"

If you need a HuggingFace token for gated models (e.g., Llama), create the secret:

kubectl create secret generic hf-token \
--namespace align \
--from-literal=token="hf_..."

See LLM Configuration - GPU Inference for full GPU setup including node requirements.

tip

GPU inference is optional. Without it, Align uses cloud APIs (OpenAI/Anthropic) or CPU-based models (Ollama). You can add GPU later without downtime.

Step 5: Install Align​

helm install runs the database migrations first, as a hook, and prints nothing until they finish. That is normal, so do not interrupt it. If it never finishes, the job cannot pull its image. The job uses global.imagePullSecrets, so check the pull secret from "Let the cluster pull the images".

helm install align \
oci://your-registry/align/charts/align \
--version "$ALIGN_CHART_VERSION" \
--namespace align \
--values values.yaml

From Align's Registry (Direct)​

helm install align \
oci://$ALIGN_REGISTRY/align/charts/align \
--version "$ALIGN_CHART_VERSION" \
--namespace align \
--values values.yaml

Step 6: Verify Installation​

# Check pods are running
kubectl get pods -n align

# Expected output:
# NAME READY STATUS RESTARTS AGE
# align-gateway-xxx 1/1 Running 0 2m
# align-brain-xxx 1/1 Running 0 2m
# align-ui-xxx 1/1 Running 0 2m
# align-connector-slack-xxx 1/1 Running 0 2m
# ...

Allow two to three minutes. A gateway in CrashLoopBackOff whose log says Cognito configuration missing has no sign-in method yet: finish OIDC in Step 8, or create the placeholder secret from Step 3. Then restart it with kubectl rollout restart deploy/align-gateway -n align.

# Check services
kubectl get svc -n align

# Check ingress
kubectl get ingress -n align

Step 7: Run Migrations​

Migrations run automatically as a Helm pre-install hook. Verify they completed:

kubectl get jobs -n align

# Should show:
# NAME COMPLETIONS DURATION AGE
# align-migrations 1/1 30s 5m

Enable the RLS Backstop​

If you set rlsCutover.enabled: true (see Step 2), this section is already done. The chart sets the password and checks the app-url connection before the gateway starts. It needs the app-password and app-url keys in align-database, which postgresql.enabled writes for you. With your own database, add both keys first. On an existing release it does not restart a running gateway: restart it once.

The migrations you just ran created a restricted align_app database role, NOLOGIN by default. Align enforces tenant isolation at two layers: the application scopes every query to one tenant, and PostgreSQL Row-Level Security filters every row by tenant_id. RLS only bites when the app connects as a non-owner role - an install still running on the url key from Step 2 has the application-layer check but no database-level backstop.

align_app cannot be created before this point (migration 073 is what creates it), which is why this is a step here rather than something Step 2 could finish on its own:

# 1. Give the restricted role a login. Run once, as the database owner/superuser -
# OWNER_URL is the "url" key already in the align-database secret from Step 2.
OWNER_URL=$(kubectl get secret align-database -n align -o jsonpath='{.data.url}' | base64 -d)
psql "$OWNER_URL" -c "ALTER ROLE align_app WITH LOGIN PASSWORD 'a-strong-password'"

# 2. Add the app-url key to the existing secret (url stays - migrations still use it).
kubectl create secret generic align-database \
--namespace align \
--from-literal=url="postgresql://user:password@host:5432/align" \
--from-literal=app-url="postgresql://align_app:a-strong-password@host:5432/align" \
--from-literal=host="your-db-host.example.com" \
--from-literal=port="5432" \
--from-literal=username="align" \
--from-literal=password="your-secure-password" \
--dry-run=client -o yaml | kubectl apply -f -

# 3. Restart the gateway so it picks up the new connection.
kubectl rollout restart deploy/align-gateway -n align

Confirm it actually took effect. The gateway uses DATABASE_APP_URL exactly as given - a wrong password or a role that never got LOGIN makes it fail to connect as align_app outright, not fall back to the owner silently. Check kubectl logs for an authentication error, or a CrashLoopBackOff on align-gateway, then roll back (remove the app-url key - see "Gateway logs authentication failures" in Troubleshooting below) while you fix it. Once it is fixed, confirm the connection directly:

psql "postgres://align_app:<align_app-password>@<host>:5432/<db>" -c "SELECT current_user;"
# expect align_app, not the owner username

This covers the gateway only. Brain still connects as the owner unless you also follow RLS Cutover's Brain section - the standard install does not wire this up for you. Omitting app-url entirely still works - the gateway falls back to url - but loses the database-level backstop. An existing install adding this later follows the same three commands; see RLS Cutover for the full runbook, the RDS Proxy notes, the fuller verification query, and rollback.

Step 8: Access Align​

What the code does​

A self-host signs people in through your identity provider, using OpenID Connect (OIDC). Three facts decide everything:

  • A tenant must exist first. The gateway does not create one at sign-in. It matches the person's email domain to a tenant, or else uses gateway.oidc.defaultTenantId. With neither, the person lands on the login page with "No tenant configured for <domain>".
  • A new user is a member. Nothing makes the first person an org_admin, on purpose: a rule like "first sign-in wins" would give the tenant to whoever reaches the page first.
  • Only an org_admin can invite others, in the web app under Settings, Team invites.

So you name the first admin up front, in part 1. The bootstrap Job creates that person as an org_admin before anyone signs in. Part 3 is the fallback for an install that already has users.

Someone who signs in on a domain with no tenant sees a neutral message. On a default install it says your organisation has not set up the workspace for that email domain, and to ask your workspace administrator. It names no Align contact. To use your own wording, set gateway.signupClosedMessage. See Unprovisioned email domains.

1. Create the tenant and the first admin​

Pick a tenant id now, so you can use it in part 2 without waiting for a log line:

uuidgen | tr 'A-Z' 'a-z'

Add this to your values file:

selfHost:
bootstrap:
enabled: true
tenantName: "Your Company"
tenantId: "<the uuid from above>"
adminEmail: "first.admin@yourdomain.com" # exactly as your identity provider sends it

The chart runs one Job, align-bootstrap, from the migrations image after the install. The Job:

  • creates your tenant with that id (a second run changes nothing),
  • creates adminEmail as an org_admin, marked manual so a group mapping never changes it,
  • deletes Align's own seed tenant, its SSO config that points at Align's login pool, and the tom@align.tech grant. It refuses, and changes nothing, if that tenant holds real rows.

It never promotes someone who already has an account. If adminEmail already exists as a member, the log says Not promoted and you use part 3. Leave tenantId empty and the Job reuses a tenant of the same name, or makes one and prints ALIGN_TENANT_ID=<uuid> in its log:

kubectl logs -n align job/align-bootstrap

The Job is named <release>-bootstrap. Do not turn this on for Align's own infrastructure: the seed removal deletes Align's tenant.

Starter rules​

With adminEmail set, a second Job (<release>-starter-rules-seed, from the gateway image) runs after the bootstrap Job and loads a starter rule pack for that first admin. Each rule is an ordinary decision. The first admin owns it and has ratified it until you reassign it, and you can edit, supersede or archive it like any other.

selfHost:
bootstrap:
starterRules:
packs: ["general", "healthcare"] # default: ["general"]. [] loads nothing.
  • general is the four day-one rules: customer details out of tickets and public channels, confirm scope before a bulk delete, no contracts pasted into unapproved AI tools, and no plain-text secrets.
  • healthcare adds three more: patient details stay out of tickets and chat, a controlled-substance reporting rule takes effect only once a named person confirms they own it, and changes to regulated code are never merged automatically.
  • A rerun of install or upgrade adds nothing. A rule that is already there is left as it is, including any edit you made.
  • Without adminEmail, no Job is created and nothing is seeded. The same holds with brain.enabled: false: every decision is redacted through the brain before it is stored, so there is nothing to seed with.
  • With it, the Job fails, and the release shows it, if the admin (who must be an org_admin) or the tenant is missing, or the brain does not answer within five minutes.
  • A rule an admin has skipped is not loaded again.
  • A rule only governs what an agent is checked against. The healthcare rules are judged by a model, not matched exactly, so they flag likely breaches and do not guarantee none.

A tenant made this way starts on the Growth plan, so the web app shows the dashboard, decisions and Discover. To start on Enterprise instead, set selfHost.bootstrap.plan: enterprise. The values are growth, enterprise and free. The Job raises a tenant that is still on the free plan and never changes one already on a paid plan. Apply your license as described in Licensing.

2. Turn on OIDC​

One-VM pilot

On the one-VM pilot, run sudo align-pilot sso instead of this step.

Create the align-oidc secret and enable OIDC. This is the whole change. Reading the client secret from a prompt keeps it out of your shell history:

read -rs CS
kubectl create secret generic align-oidc --namespace align \
--from-literal=issuer-url="https://login.yourdomain.com" \
--from-literal=client-id="<client id>" \
--from-literal=client-secret="$CS"
unset CS
gateway:
apiBaseUrl: "https://api.yourdomain.com"
frontendUrl: "https://app.yourdomain.com"
oidc:
enabled: true
defaultTenantId: "<the tenantId from part 1>"

Run helm upgrade with the same --values file plus these lines. The migration job runs again, so a missing pull secret shows up again as a hung job. Register https://api.yourdomain.com/auth/callback as the redirect URI in your identity provider, using the host in gateway.apiBaseUrl. Full provider steps are in OIDC Authentication.

Set gateway.frontendUrl to the address your users type. If you leave it out, the chart takes the first ui.ingress host. With no UI ingress it falls back to http://align.local, and every link breaks.

After the upgrade, the gateway log shows Generic OIDC initialized for self-hosted authentication. If it shows OIDC discovery failed, the gateway cannot reach your issuer URL from inside the cluster.

3. Promote someone by hand (an install that already has users)​

Skip this if part 1 named your first admin. Otherwise have the person sign in once. They are a member. Then promote them as the database owner, with the tenant id from part 1:

OWNER_URL=$(kubectl get secret align-database -n align -o jsonpath='{.data.url}' | base64 -d)
psql "$OWNER_URL" -v ON_ERROR_STOP=1 <<'SQL'
BEGIN;
SELECT set_config('app.current_tenant', '<tenant-id>', true);
UPDATE users SET role = 'org_admin', role_source = 'manual'
WHERE tenant_id = '<tenant-id>'
AND lower(email) = lower('<first-admin-email>');
COMMIT;
SQL

The output must say UPDATE 1. Anything else means the tenant id or the email is wrong. The statement limits itself to that tenant and that email, so it cannot touch another tenant. The set_config line is there for databases that enforce row-level security on the owner role.

If your web app still says member, sign out and in again.

A row promoted this way is marked manual, so a group mapping never changes it.

To grant org_admin from a directory group instead, see Map directory groups to roles.

Your identity provider must verify email addresses. The gateway refuses a sign-in when the provider says email_verified is false. A provider that leaves the claim out is accepted, so turn on email verification there before you rely on a pre-seeded admin.

4. Sign in and check the role​

  1. Make sure DNS and TLS work for both addresses.
  2. Open https://app.yourdomain.com and choose Sign in with SSO.
  3. Open Settings. A Team invites tab means you are an org_admin. No tab means you are a member: go back to part 3.

No identity provider​

Self-host sign-in uses your identity provider. The gateway also has email sign-in with a one-time code or link. It needs RESEND_API_KEY in the align-resend secret, which means an outbound call to the Resend email service. A new tenant also needs SELF_SERVE_SIGNUP_ENABLED=true.

Step 9: Connect the CLI and ask a question​

This proves the install gives an answer, which pods running does not. The person who types needs Node 22.16 or newer. Run these in a normal terminal, not inside an AI agent: the join and login commands refuse to run there.

  1. Install the CLI: npm install -g @aligndottech/cli, then align --version.

  2. As the first admin, open Settings, Team invites in the web app and choose Create invite link. Copy the align join '<link>' command shown under it. Keep the quotes.

  3. Run it. The CLI shows the two servers it is about to trust. Your gateway is not under align.tech, so it asks you to type the gateway host back. That is the check working. Type the host from gateway.apiBaseUrl.

  4. If the invite link has no gw= in it, gateway.apiBaseUrl is wrong or missing. Fix it and create a new link.

  5. No browser on that machine? In the web app open Settings, API Tokens, create a token, and run align login --env <name> --token <token>. The environment name must exist already, which align join creates. Before a join the CLI knows only local, preview and prod: any other name fails with Unknown environment. align join itself needs a terminal and exits without saving anything when its input or output is not one, so run it by hand.

  6. Run align whoami. Read the tenant name out loud. It must be your tenant. The role is org_admin for the first admin.

  7. Ask one real question. Pick a decision your team remembers well:

    align ask "Why did we choose <the thing your team decided>?"

    Expected: the answer names that decision and cites a source you recognise. An empty result is a failure.

    Set OPENAI_API_KEY on the Brain first. Without it, a question returns 503 embedding_provider_unavailable. A GPU model does not help, because it serves chat, not embeddings. See Embeddings.

  8. Ask for something that is not in your history, such as "what did we decide about quantum networking". Expected: an honest "nothing found". If it invents a decision, stop and tell us.

align ask runs from the terminal, not through an agent. Your agent reaches Align through align mcp, and the check for that path is a real tools/call of align_ask. A tools/list reply proves nothing, because it never reaches the gateway and passes with a wrong credential. Ask your agent the same question and look for the align_ask tool call in its output, not only the answer: an agent can answer from the repo and never touch Align.

If item 7 shows a 401 or an empty result, run align whoami --env <name>, then align login --env <name> again.

Troubleshooting​

Pods not starting​

# Check pod status
kubectl describe pod -n align <pod-name>

# Check logs
kubectl logs -n align <pod-name>

Database connection issues​

# Verify secret exists
kubectl get secret align-database -n align

# Test connection from a pod
kubectl run -it --rm debug --image=postgres:15 \
--restart=Never -n align -- \
psql "postgresql://user:pass@host:5432/align"

Gateway logs authentication failures for align_app​

The role has no LOGIN yet, or the password in the app-url secret key does not match what you set with ALTER ROLE. Roll back by removing the app-url key and restarting:

kubectl patch secret align-database -n align --type=json \
-p='[{"op": "remove", "path": "/data/app-url"}]'
kubectl rollout restart deploy/align-gateway -n align

The gateway falls back to the owner url connection, which has no database-level RLS backstop but is otherwise unaffected. See RLS Cutover to retry.

Ingress not working​

# Check ingress status
kubectl describe ingress -n align

# Verify TLS secret exists
kubectl get secret api-tls -n align

Next Steps​