Deploy
Run Archive on Kubernetes or a cloud, and manage what is inside it as code.
Helm
apps/helm/archive runs Archive as a one-replica StatefulSet with a persistent volume at /data, a Service and an optional Ingress, with health checks on /healthz.
helm install archive apps/helm/archive \
--set url=https://archive.example.com \
--set adminEmail=you@example.com \
--set secretKey=$(openssl rand -hex 32) \
--set ingress.enabled=true --set ingress.host=archive.example.com| Value | Default | Meaning |
|---|---|---|
image.repository, image.tag | ghcr.io/mulholland-inc/archive, the chart version | Image |
url | ARCHIVE_URL | |
adminEmail | ARCHIVE_ADMIN_EMAIL | |
secretKey | ARCHIVE_SECRET_KEY, in the chart's Secret | |
database.url | DATABASE_URL for external Postgres | |
env | {} | More environment variables |
secretEnv | {} | More environment variables, in the Secret |
existingSecret | Use your own Secret instead | |
persistence.size, persistence.storageClass | 10Gi | Volume for /data |
service.type, service.port | ClusterIP, 80 | Service |
ingress.enabled, className, host, tlsSecret, annotations | off | Ingress |
resources | {} | Container resources |
Cloud modules
apps/terraform has a module per cloud. Each runs one container of the image with the provider's managed Postgres 18, sets ARCHIVE_URL, ARCHIVE_ADMIN_EMAIL, ARCHIVE_INSTANCE and a generated ARCHIVE_SECRET_KEY, and takes extra variables in env. Read the managed Postgres note below before you use one.
| Module | Runs on | Database | Files |
|---|---|---|---|
aws | ECS Fargate behind an Application Load Balancer, optional ACM certificate | RDS for PostgreSQL 18 | S3 bucket |
gcp | Cloud Run, one instance, always on | Cloud SQL for PostgreSQL 18 | GCS bucket through HMAC keys |
azure | Container Apps | Azure Database for PostgreSQL flexible server 18, with the vector extension allowed | NFS Azure Files share at /data |
module "archive" {
source = "./apps/terraform/azure"
admin_email = "you@example.com"
env = {
ARCHIVE_LLM_URL = "https://llm.example.com/v1"
ARCHIVE_LLM_MODEL = "qwen2.5:7b"
}
}
output "url" {
value = module.archive.url
}Inputs: aws takes region, name, image_tag, admin_email, instance, env, vpc_id, subnet_ids, domain, certificate_arn, cpu, memory, db_instance_class, db_storage and deletion_protection. gcp takes project, region, cpu, memory, db_tier and the shared inputs. azure takes subscription_id, location, cpu, memory, db_sku, db_storage_mb, share_quota and the shared inputs. Each outputs url.
Managed Postgres
Check your database role first
| Cloud | Works? | Why |
|---|---|---|
| AWS RDS and Aurora | No | The master user cannot get BYPASSRLS and there is no superuser, so the aws module does not work today |
| Google Cloud SQL | Probably not | cloudsqlsuperuser is not a real superuser and is not known to bypass row-level security |
| Azure Database for PostgreSQL | Most likely | Its administrator can create roles and is expected to be allowed BYPASSRLS; check before you rely on it |
| Your own Postgres or Kubernetes | Yes | Use a superuser, or a role with CREATEROLE and BYPASSRLS |
See external Postgres for the full list of what the role needs. The bundled Postgres in the image always works, so a single container with a persistent volume, on any platform, is the safe choice.
Terraform provider
The archive provider manages what lives inside an instance through its API, so your types, sources, views, tokens and memberships can live in a repository and change by pull request. Its source is apps/terraform/provider, with the registry address mulholland-inc/archive.
terraform {
required_providers {
archive = {
source = "mulholland-inc/archive"
}
}
}
provider "archive" {
endpoint = "https://archive.example.com"
token = var.archive_token
}
resource "archive_type" "receipt" {
name = "receipt"
description = "A shop receipt"
fields = {
shop = { type = "text", required = true }
date = { type = "date", required = true }
total = { type = "money", required = true }
}
}
resource "archive_source" "scans" {
name = "scans"
kind = "folder"
type = archive_type.receipt.name
schedule = "15m"
config = jsonencode({ path = "/data/inbox" })
}The provider needs a full token from archive token create -full to manage tokens and memberships. To build it yourself, run go build in apps/terraform/provider and point Terraform at it with a dev_overrides block.
| Resource | Manages |
|---|---|
archive_type | A type and its fields, through propose, preview and apply. Renaming a field here removes the old one and its values |
archive_source | A source: name, kind, space, type, schedule, rate, config and credentials as JSON; auth_url for Google sources |
archive_view | A saved view: name, space, type, layout, by, where |
archive_token | A token: name, spaces, full, expires; token holds the secret |
archive_policy | A user's role in a space: space, user, role |
Data sources: archive_type reads one type, built-in or yours, and archive_spaces lists the spaces the token can see.