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 need | Who issues it | Where it is used |
|---|---|---|---|
| 1 | Registry host, region, chart version and release tag | Align, at onboarding | Getting the Helm Chart |
| 2 | Either 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 bundle | Authenticate to the registry, Offline install |
| 3 | license.jwt | Align | Licensing |
| 4 | An 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 |
| 5 | Kubernetes 1.25+, Helm 3.10+, PostgreSQL 15+ with pgvector, Redis 6+, an ingress controller, two DNS names and TLS | You (your platform team) | Prerequisites |
| 6 | Two public addresses: the API (gateway.apiBaseUrl) and the web app (gateway.frontendUrl). They go into every invite link and OAuth callback. | You choose them | Step 4 |
| 7 | An OpenID Connect app in your identity provider: issuer URL, client ID, client secret | You (your identity provider admin) | Step 8 |
| 8 | The email of the person who becomes the first admin | You decide | Step 8 |
| 9 | OAuth apps for each connector you enable (Slack, Jira, GitHub and so on). Your own apps, one per tool. | Your admin registers each one | Connector Authentication |
| 10 | A laptop with a terminal, and Node 22.16 or newer, for the last step | You | Step 9 |
| 11 | A 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
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
alignnamespace 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
Option A: OCI Registry (Recommended)
# 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
For production deployments, we recommend mirroring artifacts to your own registry (Option B below).
Option B: Mirror to Your Registry (Recommended for Production)
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.
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+
-
kubectlconfigured 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 upgradedoes not change them andhelm uninstallleaves them, and the data volume, in place. Delete them by hand to start clean. postgresql.enabledneedsrlsCutover.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 templatecannot read them back, so under GitOps setpostgresql.auth.password,postgresql.auth.appPasswordandsecrets.internal.masterEncryptionKeyyourself. - Turn it on at the first install. Enabling
postgresqlon an existing release does nothing, because install hooks do not run on upgrade. The chart also refuses to overwrite an existingalign-databaseSecret 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.
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".
From Your Mirrored Registry (Recommended)
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 anorg_admin, on purpose: a rule like "first sign-in wins" would give the tenant to whoever reaches the page first. - Only an
org_admincan 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
adminEmailas anorg_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.techgrant. 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.
generalis 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.healthcareadds 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 withbrain.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
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
- Make sure DNS and TLS work for both addresses.
- Open
https://app.yourdomain.comand choose Sign in with SSO. - Open Settings. A Team invites tab means you are an
org_admin. No tab means you are amember: 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.
-
Install the CLI:
npm install -g @aligndottech/cli, thenalign --version. -
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. -
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 fromgateway.apiBaseUrl. -
If the invite link has no
gw=in it,gateway.apiBaseUrlis wrong or missing. Fix it and create a new link. -
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, whichalign joincreates. Before a join the CLI knows onlylocal,previewandprod: any other name fails withUnknown environment.align joinitself needs a terminal and exits without saving anything when its input or output is not one, so run it by hand. -
Run
align whoami. Read the tenant name out loud. It must be your tenant. The role isorg_adminfor the first admin. -
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_KEYon the Brain first. Without it, a question returns503 embedding_provider_unavailable. A GPU model does not help, because it serves chat, not embeddings. See Embeddings. -
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
- Configure OAuth Apps for Slack, Teams, etc.
- Set up LLM for decision synthesis
- Configure secrets properly for production