Home

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
ValueDefaultMeaning
image.repository, image.tagghcr.io/mulholland-inc/archive, the chart versionImage
urlARCHIVE_URL
adminEmailARCHIVE_ADMIN_EMAIL
secretKeyARCHIVE_SECRET_KEY, in the chart's Secret
database.urlDATABASE_URL for external Postgres
env{}More environment variables
secretEnv{}More environment variables, in the Secret
existingSecretUse your own Secret instead
persistence.size, persistence.storageClass10GiVolume for /data
service.type, service.portClusterIP, 80Service
ingress.enabled, className, host, tlsSecret, annotationsoffIngress
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.

ModuleRuns onDatabaseFiles
awsECS Fargate behind an Application Load Balancer, optional ACM certificateRDS for PostgreSQL 18S3 bucket
gcpCloud Run, one instance, always onCloud SQL for PostgreSQL 18GCS bucket through HMAC keys
azureContainer AppsAzure Database for PostgreSQL flexible server 18, with the vector extension allowedNFS 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

Archive's database role must bypass row-level security and create roles. Not every managed Postgres allows that, and Archive does not start without it.
CloudWorks?Why
AWS RDS and AuroraNoThe master user cannot get BYPASSRLS and there is no superuser, so the aws module does not work today
Google Cloud SQLProbably notcloudsqlsuperuser is not a real superuser and is not known to bypass row-level security
Azure Database for PostgreSQLMost likelyIts administrator can create roles and is expected to be allowed BYPASSRLS; check before you rely on it
Your own Postgres or KubernetesYesUse 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.

ResourceManages
archive_typeA type and its fields, through propose, preview and apply. Renaming a field here removes the old one and its values
archive_sourceA source: name, kind, space, type, schedule, rate, config and credentials as JSON; auth_url for Google sources
archive_viewA saved view: name, space, type, layout, by, where
archive_tokenA token: name, spaces, full, expires; token holds the secret
archive_policyA 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.