unoverse 0.1.69 → 0.1.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ # Everything .env.production needs — the rendered file is COMPLETE, nothing
2
+ # is hand-edited afterwards:
3
+ # terraform output -raw env_production > ../../.env.production
4
+ # unoverse deploy
5
+
6
+ locals {
7
+ # Pooled URL for the services (transaction-mode PgBouncer; prepared statements off),
8
+ # direct URL for migrations. BYO: your URL is used verbatim for both — you own the
9
+ # pooling story against your own max_connections (INFRASTRUCTURE.md § BYO).
10
+ # Managed modes (provisioned OR adopted existing cluster): the universe's own
11
+ # user/db/pool were created on local.pg_cluster_id either way.
12
+ pg_pooled = local.pg_managed ? "postgresql://${digitalocean_database_user.universe[0].name}:${digitalocean_database_user.universe[0].password}@${digitalocean_database_connection_pool.universe[0].private_host}:${digitalocean_database_connection_pool.universe[0].port}/${digitalocean_database_connection_pool.universe[0].name}?sslmode=require&pgbouncer=true" : var.byo_postgres_url
13
+ pg_direct = local.pg_managed ? "postgresql://${digitalocean_database_user.universe[0].name}:${digitalocean_database_user.universe[0].password}@${local.pg_cluster_host}:${local.pg_cluster_port}/${digitalocean_database_db.universe[0].name}?sslmode=require" : var.byo_postgres_url
14
+
15
+ # Redis is always ours — provisioned above, no BYO branch.
16
+ redis_host = digitalocean_database_cluster.redis.private_host
17
+ redis_port = digitalocean_database_cluster.redis.port
18
+ redis_password = digitalocean_database_cluster.redis.password
19
+ redis_tls = true
20
+ }
21
+
22
+ output "deploy_host" {
23
+ description = "The droplet's public IP (.env.production DEPLOY_HOST). Point api.<domain> at the LB IP, not this."
24
+ value = digitalocean_droplet.app.ipv4_address
25
+ }
26
+
27
+ output "lb_ip" {
28
+ description = "The load balancer's IP — api.<domain>'s A record target (created automatically when manage_dns = true)."
29
+ value = digitalocean_loadbalancer.public.ip
30
+ }
31
+
32
+ output "canvas_url" {
33
+ description = "Public Canvas URL (canvas_public = true only) — add it to the IdP's allowed origins."
34
+ value = var.canvas_public ? (local.has_domain ? "https://canvas.${var.domain}:3001" : "http://${digitalocean_loadbalancer.public.ip}:3001") : "canvas is admin-only (direct http://<droplet-ip>:3001 from admin_cidr)"
35
+ }
36
+
37
+ output "api_url" {
38
+ description = "The API base URL as deployed — https://api.<domain> with a domain, http://<lb-ip> without one."
39
+ value = local.has_domain ? "https://${local.api_host}" : "http://${digitalocean_loadbalancer.public.ip}"
40
+ }
41
+
42
+ output "env_production" {
43
+ sensitive = true
44
+ description = "Rendered .env.production — complete, write to a file and deploy"
45
+ value = <<-ENV
46
+ # UNIVERSE (DigitalOcean) — generated by infra/digitalocean. COMPLETE:
47
+ # do not hand-edit; change terraform.tfvars and re-render instead.
48
+
49
+ # Deploy target
50
+ DEPLOY_HOST=${digitalocean_droplet.app.ipv4_address}
51
+ DEPLOY_USER=root
52
+
53
+ # Container registry
54
+ DOCR_TOKEN=${var.docr_token}
55
+
56
+ # Database (pooled via managed PgBouncer, transaction mode — the Postgres law).
57
+ # Migrations use the DIRECT url (db-setup prefers DATABASE_URL_DIRECT).
58
+ DATABASE_URL=${local.pg_pooled}
59
+ DATABASE_URL_DIRECT=${local.pg_direct}
60
+
61
+ # Connection budget (INFRASTRUCTURE.md, size = ${var.size})
62
+ DB_POOL_ENGINE=${local.s.pool_engine}
63
+ DB_POOL_ENGINE_LEGACY=${local.s.pool_legacy}
64
+ DB_POOL_MEMORY=${local.s.pool_memory}
65
+
66
+ # Credential encryption at rest — per-deployment, generated by Terraform.
67
+ # Back this up with the database (a DB backup is unreadable without it).
68
+ CREDENTIAL_ENCRYPTION_KEY=${random_password.credential_key.result}
69
+
70
+ # Redis (managed, TLS)
71
+ REDIS_HOST=${local.redis_host}
72
+ REDIS_PORT=${local.redis_port}
73
+ REDIS_PASSWORD=${local.redis_password}
74
+ REDIS_TLS=${local.redis_tls}
75
+ REDIS_NAMESPACE=universe
76
+
77
+ # OIDC — byo (Auth0 today): the tenant is authoritative for roles/permissions.
78
+ AUTH_ISSUER=${var.auth_issuer}
79
+ AUTH_CLIENT_ID=${var.auth_client_id}
80
+ AUTH_AUDIENCE=${var.auth_audience}
81
+
82
+ # Service keys
83
+ OPENAI_API_KEY=${var.openai_api_key}
84
+ HYPERBROWSER_API_KEY=${var.hyperbrowser_api_key}
85
+
86
+ # Ingress. With a domain: DOMAIN drives every URL (compose derives
87
+ # https://api.<domain>). Without one (POC): DOMAIN stays empty and the
88
+ # explicit URLs below point at the load balancer's IP over plain HTTP.
89
+ # To upgrade later: set domain in terraform.tfvars, terraform apply,
90
+ # re-render this file, unoverse deploy. Nothing is destroyed.
91
+ DOMAIN=${var.domain}
92
+ ${local.has_domain ? "" : "API_URL=http://${digitalocean_loadbalancer.public.ip}"}
93
+ ${local.has_domain ? "" : "VITE_SERVER_WS_URL=ws://${digitalocean_loadbalancer.public.ip}"}
94
+ ${local.has_domain ? "" : "UNOVERSE_URL=http://${digitalocean_loadbalancer.public.ip}"}
95
+ ENV
96
+ }
@@ -0,0 +1,49 @@
1
+ # What this ground costs to run, priced where its sizes are chosen.
2
+ #
3
+ # ONE HOME FOR THE NUMBERS. These used to live in the CLI's plan summariser and again in
4
+ # the docs, so a size change here left two stale copies behind and the deploy screen
5
+ # disagreed with the page describing it. The ground owns its sizes, so the ground prices
6
+ # them: `unoverse deploy` reads this map out of the plan JSON, and the docs quote the
7
+ # same output rather than a table somebody keeps in step by hand.
8
+ #
9
+ # NOT A COST ORACLE. DigitalOcean's published monthly rates for exactly the slugs this
10
+ # ground uses, so a developer sees an order of magnitude instead of a surprise. Rates
11
+ # move; the bill is the provider's.
12
+
13
+ locals {
14
+ # Monthly USD, keyed by the size slug it prices. A slug absent here is simply not
15
+ # priced (the summary shows the resource with no figure) rather than guessed at.
16
+ prices = {
17
+ # Droplets
18
+ "s-4vcpu-16gb-amd" = 96 # small: the POC box
19
+ "g-8vcpu-32gb" = 252 # medium
20
+ "m-8vcpu-64gb" = 386 # large
21
+ # Managed Postgres and Redis
22
+ "db-s-1vcpu-1gb" = 15
23
+ "db-s-1vcpu-2gb" = 30
24
+ "db-s-2vcpu-4gb" = 60
25
+ "db-s-4vcpu-8gb" = 120
26
+ # Fixed-price resources, keyed by what they are rather than a size
27
+ "lb" = 12
28
+ }
29
+
30
+ # The steady-state bill for THIS universe, as configured. Adopted or bring-your-own
31
+ # Postgres is not in it: that cluster is somebody else's line item, and pricing it here
32
+ # would invent a charge this stack does not create.
33
+ monthly_estimate = sum([
34
+ lookup(local.prices, local.s.droplet, 0),
35
+ local.provision_pg ? lookup(local.prices, local.s.pg, 0) : 0,
36
+ lookup(local.prices, local.s.redis, 0),
37
+ local.prices["lb"],
38
+ ])
39
+ }
40
+
41
+ output "prices" {
42
+ description = "Monthly USD by size slug. The CLI's plan summary reads this, so the deploy screen and the docs cannot drift apart."
43
+ value = local.prices
44
+ }
45
+
46
+ output "monthly_estimate" {
47
+ description = "Roughly what this universe costs a month at list prices, for the size and Postgres mode configured."
48
+ value = local.monthly_estimate
49
+ }
@@ -0,0 +1,52 @@
1
+ # Copy to terraform.tfvars and fill in — or let `./unoverse ground do` write it
2
+ # for you, prefilled from doctl (your IP, SSH key, existing Postgres clusters).
3
+ # terraform.tfvars is gitignored.
4
+ # Token: set DIGITALOCEAN_TOKEN in the environment (preferred) or do_token here.
5
+
6
+ region = "lon1"
7
+ name = "universe-poc"
8
+ size = "small" # small (POC) | medium | large
9
+ admin_cidr = "203.0.113.7/32" # YOUR IP — SSH + Dozzle only
10
+ ssh_key_name = "your-do-ssh-key-name" # must already exist in the DO account
11
+ # Domain is OPTIONAL. Leave it empty and the universe comes up on the load
12
+ # balancer's IP over plain HTTP (POC mode) — `terraform output api_url` tells
13
+ # you the address. When you have a domain, fill it in and `terraform apply`
14
+ # again: the SAME deployment upgrades in place to TLS at api.<domain>.
15
+ domain = "" # empty = HTTP on the LB IP; "yourdomain.com" = TLS at api.yourdomain.com
16
+ manage_dns = false # true only if the domain's DNS is on DO
17
+
18
+ # ⚠ With a domain set, the managed Let's Encrypt certificate requires the
19
+ # domain's DNS to be HOSTED on DigitalOcean (registrar can stay GoDaddy etc. —
20
+ # point the nameservers at ns1/ns2/ns3.digitalocean.com and add the domain
21
+ # under Networking). Then manage_dns = true also creates the records for you.
22
+
23
+ # POC ONLY: public Canvas at https://api.<domain>:3001 (a second port on the ONE LB).
24
+ # Add that URL to the IdP's allowed origins. Default false = admin-only.
25
+ canvas_public = true
26
+
27
+ # Auth — byo-oidc (the standing choice: DO universes use the existing Auth0
28
+ # tenant; roles/permissions live THERE, Terraform doesn't touch them).
29
+ auth_issuer = "https://your-tenant.auth0.com"
30
+ auth_client_id = "your-spa-client-id"
31
+ auth_audience = "gravity-api"
32
+
33
+ # Service secrets — with these set, the rendered .env.production is COMPLETE.
34
+ docr_token = "dop_v1_..." # registry token (pulls platform images)
35
+ openai_api_key = "sk-..." # memory server + OpenAI nodes
36
+ # hyperbrowser_api_key = "" # optional — page intelligence
37
+
38
+ # Postgres — three OPTIONAL modes (leave all unset = provision a fresh cluster):
39
+ # Reuse YOUR existing DO Postgres (the usual case) — Terraform adds the
40
+ # universe's own db/user/pool to it, touching nothing else. Name from
41
+ # `doctl databases list`:
42
+ # existing_pg_cluster_name = "your-existing-cluster-name"
43
+ # Or a fully external database, used verbatim:
44
+ # byo_postgres_url = "postgresql://user:pass@host:5432/db?sslmode=require"
45
+
46
+ # Redis: always provisioned by Terraform (Managed Redis, TLS). Not configurable.
47
+
48
+ # The rendered output (terraform output -raw env_production > ../../.env.production)
49
+ # IS your universe's secrets, including the MASTER KEY (CREDENTIAL_ENCRYPTION_KEY)
50
+ # that locks every credential users save in the platform. If it's lost, no backup
51
+ # can bring them back. Never commit it; keep a safe copy with your database backups.
52
+ # To change any value: edit this file and re-render. Never hand-edit the output.
@@ -0,0 +1,125 @@
1
+ variable "do_token" {
2
+ description = "DigitalOcean API token (or set DIGITALOCEAN_TOKEN in the environment and leave this empty)."
3
+ type = string
4
+ default = ""
5
+ sensitive = true
6
+ }
7
+
8
+ variable "region" {
9
+ description = "DigitalOcean region for everything."
10
+ type = string
11
+ default = "lon1"
12
+ }
13
+
14
+ variable "name" {
15
+ description = "Prefix for every resource."
16
+ type = string
17
+ default = "universe-poc"
18
+ }
19
+
20
+ variable "size" {
21
+ description = "Deployment size (INFRASTRUCTURE.md size table): small = POC box."
22
+ type = string
23
+ default = "small"
24
+ validation {
25
+ condition = contains(["small", "medium", "large"], var.size)
26
+ error_message = "size must be small, medium or large."
27
+ }
28
+ }
29
+
30
+ variable "admin_cidr" {
31
+ description = "The ONLY CIDR allowed to SSH (22) and view logs (Dozzle 8080). Your IP as x.x.x.x/32."
32
+ type = string
33
+ }
34
+
35
+ variable "ssh_key_name" {
36
+ description = "Name of an SSH key already uploaded to the DigitalOcean account."
37
+ type = string
38
+ }
39
+
40
+ variable "domain" {
41
+ description = "OPTIONAL. Empty (the default) = no certificate and no hostnames: the LB serves plain HTTP on its IP — the zero-friction first apply. Set it later and re-apply to upgrade IN PLACE to TLS at api.<domain> (the DNS A record is created only when manage_dns = true). Nothing is destroyed by the upgrade."
42
+ type = string
43
+ default = ""
44
+ }
45
+
46
+ variable "manage_dns" {
47
+ description = "true = the domain's DNS is hosted on DigitalOcean and Terraform manages the records. false = you point api.<domain> at the LB IP yourself (the LB cert still needs the record in place before it can issue)."
48
+ type = bool
49
+ default = false
50
+ }
51
+
52
+ # ── Auth: DigitalOcean universes run byo-oidc (Auth0 today) — the tenant is
53
+ # authoritative for roles/permissions; Terraform only passes the pointers through
54
+ # (INFRASTRUCTURE.md: roles are PROVISIONED only under Cognito on AWS).
55
+ variable "auth_issuer" {
56
+ description = "OIDC issuer, e.g. https://your-tenant.auth0.com"
57
+ type = string
58
+ }
59
+
60
+ variable "auth_client_id" {
61
+ description = "OIDC SPA client id (public by definition)."
62
+ type = string
63
+ }
64
+
65
+ variable "auth_audience" {
66
+ description = "OIDC audience (the Auth0 API identifier, e.g. gravity-api)."
67
+ type = string
68
+ default = "gravity-api"
69
+ }
70
+
71
+ # ── Postgres: three modes, first match wins ───────────────────────────────────
72
+ # 1. byo_postgres_url — fully external DB, URL used verbatim (you own
73
+ # pooling, backups, reachability).
74
+ # 2. existing_pg_cluster_name — an EXISTING DO managed cluster (the usual case:
75
+ # "I already have Postgres in DO"): Terraform creates
76
+ # the universe's OWN database, user, transaction
77
+ # pool, and firewall rule ON that cluster. Your
78
+ # existing databases are untouched — but the
79
+ # cluster's max_connections is now SHARED between
80
+ # your existing workload and this universe's pools;
81
+ # on the 1GB tier (~22 connections) watch the sum.
82
+ # 3. neither set — Terraform provisions a fresh cluster + pool.
83
+ variable "byo_postgres_url" {
84
+ description = "Mode 1: fully external Postgres URL (PG 14+, include sslmode=require). Empty = see existing_pg_cluster_id / provision."
85
+ type = string
86
+ default = ""
87
+ sensitive = true
88
+ }
89
+
90
+ variable "existing_pg_cluster_name" {
91
+ description = "Mode 2: the NAME of an EXISTING DO managed Postgres cluster (doctl databases list). Terraform adds the universe's db/user/pool/firewall to it and creates no cluster."
92
+ type = string
93
+ default = ""
94
+ }
95
+
96
+ variable "canvas_public" {
97
+ description = "POC ONLY (decided 2026-07-29): Canvas publicly at https://api.<domain>:3001 — a second port on the ONE load balancer (no second LB, no extra cost; DO LBs cannot host-route, and a clean hostname would cost a second LB). false = Canvas stays admin-only direct :3001 (the standing posture). Add https://api.<domain>:3001 to the IdP's allowed origins."
98
+ type = bool
99
+ default = false
100
+ }
101
+
102
+ # Redis is NOT flexible: always provisioned by Terraform (Managed Redis, TLS).
103
+ # It is the platform's shared-state backbone (and the active/active future) —
104
+ # the stack owns its version and locality. Only Postgres is BYO-able.
105
+
106
+ # ── Service secrets: taken here so the rendered .env.production is COMPLETE —
107
+ # the operator fills terraform.tfvars once and never hand-edits the env file.
108
+ variable "docr_token" {
109
+ description = "Container registry token — pulls the platform images."
110
+ type = string
111
+ sensitive = true
112
+ }
113
+
114
+ variable "openai_api_key" {
115
+ description = "OpenAI API key — memory server + OpenAI nodes."
116
+ type = string
117
+ sensitive = true
118
+ }
119
+
120
+ variable "hyperbrowser_api_key" {
121
+ description = "Optional — page-intelligence features. Empty = feature off."
122
+ type = string
123
+ default = ""
124
+ sensitive = true
125
+ }