Vendor, contractor, systems & supply-chain-security management for the Head of IT. Purpose-built to avoid the rigidity and bloat of off-the-shelf GRC tools.
- What it does: tracks vendors (subscriptions, seats, contacts), contractors (engagements, staff, calendar meetings), internal systems, projects, and their interdependencies; stores contracts with versioning and Claude-powered review; surfaces dependency/container CVEs; and maps who can access what.
- Next.js 16 (App Router, Server Actions, Route Handlers,
proxy.ts) + React 19 - PostgreSQL via Drizzle ORM
- Microsoft Entra ID auth (Auth.js v5) with a DB allowlist + RBAC
- Temporal for scheduling & durable background workflows (always-on worker)
- GitHub App + OSV.dev for dependency/CVE intel; Microsoft Defender for Cloud for container image scanning; Claude for contract review; Microsoft Graph for calendar + email
- Azure hosting inside a private VNet (Container Apps, Postgres Flexible Server, Blob, Key Vault, ACR)
- Tailwind v4 UI
apps/
web/ Next.js app (UI, Server Actions, Route Handlers, proxy.ts)
worker/ Temporal worker (workflows + activities)
packages/
core/ Domain enums, RBAC, money math, AES-GCM crypto (framework-free)
db/ Drizzle schema, migrations, seed, settings service
integrations/ GitHub, OSV, Defender, Claude, Blob, Graph, Key Vault clients
infra/ Azure Bicep IaC (VNet, ACR, Key Vault, Postgres, Storage, ACA env)
.github/workflows/ ci.yml (PR gate) + deploy.yml (merge-to-main release)
Prereqs: Node 24, pnpm 10, Docker.
pnpm install
cp .env.example .env # see below for the minimum to fill in
pnpm infra:up # Postgres + Temporal dev server (docker)
pnpm db:migrate # apply schema
pnpm db:seed # license catalog + a little demo data
pnpm dev # web on :3000, worker polling TemporalWeb UI: http://localhost:3000 · Temporal UI: http://localhost:8233
Minimum local .env: DATABASE_URL (default works with docker), AUTH_SECRET
(openssl rand -base64 32), and OVERLORD_ENCRYPTION_KEY (openssl rand -base64 32).
Integration credentials (GitHub, Claude, Temporal Cloud) are configured at runtime in
Settings, not env. Blob storage falls back to a local .overlord-blobs/ folder when
AZURE_STORAGE_ACCOUNT is unset.
- Auth locally: real sign-in needs Entra credentials in
.env. For quick UI work or the e2e suite, setE2E_BYPASS_AUTH=1to be treated as an owner. - First run: the first person to sign in via Entra becomes the owner and is sent
to
/setup. No bootstrap env var.
pnpm typecheck # all packages
pnpm lint # prettier --check
pnpm db:check # migration/schema drift
pnpm test # unit + integration (vitest, needs Postgres for db tests)
pnpm test:e2e # Playwright (builds + starts the app)This is the complete path from an empty Azure subscription + GitHub repo to a running,
custom-domain deployment. Do the one-time setup (steps 1–7) once; after that, every push
to main deploys automatically (step 8).
Prereqs: an Azure subscription (Owner or Contributor + User Access Administrator),
the Azure CLI (az login), an Entra tenant you can register apps in, a GitHub
repo hosting this code, and a domain you control.
Naming below assumes the overlord prefix and resource group overlord-prod — adjust to
taste (the Bicep namePrefix param and the env: block in deploy.yml must agree).
az group create -n overlord-prod -l eastus
az deployment group create -g overlord-prod -f infra/main.bicep \
-p adminObjectId="$(az ad signed-in-user show --query id -o tsv)" \
-p postgresAdminPassword='<a-strong-password>'This creates (see infra/README.md for detail): a VNet with subnets,
ACR (Premium), Key Vault, Log Analytics + Container Apps environment (VNet-
injected), Postgres Flexible Server (private access), Storage + a documents
container, and private endpoints for KV/Storage/ACR with public access disabled. Note
the outputs (acrLoginServer, keyVaultName, postgresFqdn, storageAccountName).
Create overlord-web and overlord-worker on the Container Apps environment from step 1,
each with a system-assigned managed identity, then grant roles:
| Identity | Role | Scope |
|---|---|---|
| web and worker | Key Vault Secrets User | the Key Vault |
| web | Storage Blob Data Contributor | the storage account |
| web + worker | AcrPull | the ACR |
| web | Container Apps Contributor (or a custom restart role) | the worker app (lets "Save & restart worker" work) |
# example role assignment
WEB_MI=$(az containerapp show -g overlord-prod -n overlord-web --query identity.principalId -o tsv)
az role assignment create --assignee "$WEB_MI" --role "Key Vault Secrets User" \
--scope $(az keyvault show -n overlord-kv --query id -o tsv)- Entra admin center → App registrations → New registration.
- Redirect URI (Web):
https://<APP_BASE_URL>/api/auth/callback/microsoft-entra-id(e.g.https://overlord.yourco.com/...). You can add it now with your intended domain. - Certificates & secrets → New client secret — copy the value.
- API permissions → Microsoft Graph → Delegated:
User.Read,Calendars.Read,offline_access; then Grant admin consent. - Record: Application (client) ID, Directory (tenant) ID, the client secret,
and the issuer
https://login.microsoftonline.com/<tenant-id>/v2.0. - Email alerts (optional but recommended). So the worker can email renewal / CVE
alerts to owners + admins, grant the worker Container App's managed identity the
Microsoft Graph
Mail.Sendapplication permission and admin-consent it (assign the Graph app role to the identity's service principal, e.g. viaaz ad app permission/ Microsoft Graph PowerShell). SetAZURE_TENANT_IDon the worker, and set the sender mailbox at first-run under Settings (it must be a real licensed mailbox). Without this, alerts still appear in-app and can post to Teams; only email is skipped.
Create a federated credential so GitHub Actions can authenticate to Azure without a secret:
- Create (or reuse) an app registration / user-assigned identity for CI, and give it
Contributor on
overlord-prod. - Add a federated credential for this repo's
productionenvironment (subjectrepo:<org>/<repo>:environment:production). - You'll use its client id, plus the tenant id and subscription id, as repo secrets (next step).
In Settings → Secrets and variables → Actions (scope to the production environment
where appropriate):
Secrets (sensitive — synced to Key Vault by the deploy, or used by the pipeline):
| Secret | How to obtain |
|---|---|
DATABASE_URL |
postgresql://overlord:<pw>@<postgresFqdn>:5432/overlord?sslmode=require (Postgres FQDN from step 1) |
AUTH_SECRET |
openssl rand -base64 32 |
OVERLORD_ENCRYPTION_KEY |
openssl rand -base64 32 (32-byte key that encrypts integration secrets at rest) |
AUTH_MICROSOFT_ENTRA_ID_SECRET |
Entra client secret (step 3) |
AZURE_CLIENT_ID |
CI app/identity client id (step 4) |
AZURE_TENANT_ID |
your tenant id |
AZURE_SUBSCRIPTION_ID |
your subscription id |
Variables (non-sensitive — injected as container envs):
| Variable | Value |
|---|---|
APP_BASE_URL |
the public HTTPS URL, e.g. https://overlord.yourco.com (also set as AUTH_URL) |
AZURE_STORAGE_ACCOUNT |
storageAccountName output from step 1 |
Entra client id / tenant / issuer are needed by the running web app too — set
AUTH_MICROSOFT_ENTRA_ID_IDandAUTH_MICROSOFT_ENTRA_ID_ISSUERas container env vars on the web app (or add them to the deploy's--set-env-vars). Integration credentials for GitHub, Claude, and Temporal are not set here — they're entered in the Settings UI after first login and stored encrypted / in Key Vault by the app itself.
Point your domain at the web Container App and bind a managed certificate:
-
Get the app's default FQDN and the domain-verification id:
az containerapp show -g overlord-prod -n overlord-web \ --query properties.configuration.ingress.fqdn -o tsv az containerapp show -g overlord-prod -n overlord-web \ --query properties.customDomainVerificationId -o tsv
-
Create these DNS records at your provider:
Type Name Value CNAMEoverlord(your subdomain)the app's <...>.azurecontainerapps.ioFQDNTXTasuid.overlordthe customDomainVerificationId value (For an apex/root domain use an
A/ALIASto the environment's static IP instead of aCNAME.) -
Bind the domain + managed certificate:
az containerapp hostname add -g overlord-prod -n overlord-web --hostname overlord.yourco.com az containerapp hostname bind -g overlord-prod -n overlord-web \ --hostname overlord.yourco.com --environment overlord-env --validation-method CNAME -
Set
APP_BASE_URL(step 5) to the finalhttps://overlord.yourco.comand make sure the Entra redirect URI (step 3) matches.
In Microsoft Defender for Cloud → Environment settings → your subscription, turn on the Containers plan so ACR images are scanned. The worker's container-findings sync reads the results via Azure Resource Graph.
Push to main (or run the Deploy workflow manually). deploy.yml runs, in order:
- Sync Secrets — OIDC login to Azure, push the GitHub secrets into Key Vault.
- Build & push — build
overlord-web/overlord-workerimages to ACR (tagged with the commit SHA). - Migrate — run
pnpm db:migrateagainst the production database. - Deploy — roll the web + worker Container Apps to the new revision (injecting
APP_BASE_URL/AUTH_URL+ storage/KV reference envs) and health-check/api/health.
main is branch-protected; PRs must pass ci.yml (typecheck, lint, drift check, unit +
e2e tests) before merge.
- Visit
https://overlord.yourco.comand sign in with Entra — the first sign-in becomes the owner and lands on/setup(set notification sender + reporting currency). - In Settings:
- GitHub → Create GitHub App (generates the App + private key automatically; install it on your org), or paste an existing App's details.
- Claude → paste an API key (enables contract review).
- Temporal → point at your Temporal Cloud namespace + API key. Saving restarts the worker and auto-registers default schedules (security sync, container sync, renewal reminders) — tune them under Settings → Schedules.
- Notifications → optionally paste a Microsoft Teams Incoming Webhook URL to also
post alerts to a channel. Renewal/CVE alerts always appear in-app (bell +
/notifications) and are emailed to owners + admins when GraphMail.Sendis configured (step 3.6). - Access → add allowlisted app users + roles, and the internal-people directory. Alert emails go to every active owner/admin here.
Browser ─▶ Next.js web (Container App) ─▶ Postgres (private)
│ ├─▶ Blob (Managed Identity) — contract PDFs
│ ├─▶ Key Vault — secrets (Managed Identity)
│ └─▶ Temporal client — start/query workflows + schedules
▼
Microsoft Entra (Auth.js) Temporal ◀── worker (Container App)
├─▶ GitHub App (Octokit)
├─▶ OSV.dev (advisories)
├─▶ Defender for Cloud (ARG)
└─▶ Claude (contract review)
Configuration & integration credentials live in Postgres (secrets AES-GCM-encrypted or in Key Vault), managed at runtime through the Settings UI — only infrastructure secrets are in GitHub/Key Vault.
See SECURITY.md for the disclosure process and full operational warnings. The essentials:
- Secrets are never in code or committed env. Three infra secrets (
DATABASE_URL,AUTH_SECRET,OVERLORD_ENCRYPTION_KEY) flow GitHub Secrets → Key Vault at deploy; all integration credentials are entered in Settings and stored AES-256-GCM-encrypted (or in Key Vault). Secret fields are write-only in the UI and never rendered back. - The
overlord:overlordPostgres credentials and the fixed test secrets are localhost/CI only — production sources every secret from Key Vault via Managed Identity. E2E_BYPASS_AUTHmust never be set in production. It grants synthetic owner access for local dev / e2e; it is automatically inert once Entra is configured and logs a loud startup warning if ever active.- First Entra sign-in on an empty database becomes the owner — deploy to a trusted tenant and claim it yourself before sharing the URL.
- Known limitations (acceptable for v1):
next-authis on a v5 beta; there is no application-level rate limiting (relies on Entra + ingress); delegated Graph tokens live in the HttpOnly session JWT; the Teams webhook URL is admin-configured and treated as trusted.
MIT. The bundled SPDX license catalog
(packages/db/src/data/spdx-licenses.json) is
derived from the public-domain SPDX license list.