Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,9 @@ jobs:
push: false
load: true
tags: omadia-web-ui:ci-${{ github.sha }}
# Same MIDDLEWARE_URL placeholder the production fly.toml uses;
# only baked into the standalone bundle, not used at runtime
# against this image.
build-args: |
MIDDLEWARE_URL=http://middleware:8080
# No MIDDLEWARE_URL build-arg: it is a runtime setting since the
# /bot-api and /p proxies became route handlers
# (app/_lib/middlewareProxy.ts).
cache-from: type=gha,scope=web-ui
# See the middleware job: a cache-export error is non-fatal.
cache-to: type=gha,scope=web-ui,mode=max,ignore-error=true
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/desktop-apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ jobs:
npm ci
npm run build

- name: Build web-ui (bake the fixed kernel URL into the standalone build)
# No MIDDLEWARE_URL at build time: the /bot-api and /p proxies are route
# handlers that read it per request (web-ui/app/_lib/middlewareProxy.ts).
# The desktop supervisor injects the real kernel URL when it spawns the
# standalone server (desktop/src/supervisor.ts, uiEnv).
- name: Build web-ui
working-directory: web-ui
env:
MIDDLEWARE_URL: ${{ env.KERNEL_URL }}
run: |
npm ci
npm run build
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/publish-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ jobs:
image: ghcr.io/byte5ai/omadia-web-ui
context: ./web-ui
dockerfile: ./web-ui/Dockerfile
build_args: |
MIDDLEWARE_URL=http://middleware:8080
# MIDDLEWARE_URL is a runtime setting since the /bot-api and /p
# proxies became route handlers (app/_lib/middlewareProxy.ts) —
# nothing middleware-related is baked at build time anymore.
build_args: ''
steps:
- uses: actions/checkout@v7
with:
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,28 @@ the differentiating logic, and verifying with the smoke runner before install.
## Deployment

- **Local / single-tenant**: `docker compose up`, see Quickstart above
- **One-click cloud**: deploy the minimal core into your own Render
workspace — [`render.yaml`](render.yaml) provisions the middleware,
admin UI, and Postgres (pgvector), generates `VAULT_KEY`, and the
`/setup` wizard collects your LLM key on first boot. Runs on paid
instance types (the middleware needs a persistent disk).

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/byte5ai/omadia)

- **One-command Fly.io**: Fly has no blueprint-style deploy button, so the
equivalent is one command. [`fly/deploy.sh`](fly/deploy.sh) provisions
three apps in your Fly org — middleware (persistent `/data` volume),
admin UI, and a private [`pgvector/pgvector`](https://hub.docker.com/r/pgvector/pgvector)
Postgres (the same image the compose stack uses; Fly's own Postgres
offerings either lack pgvector or gate it behind a dashboard toggle) —
generates `VAULT_KEY` and the database password, and deploys the GHCR
images. Needs a logged-in `flyctl`; roughly $10/month:

```bash
git clone https://github.com/byte5ai/omadia.git && cd omadia
./fly/deploy.sh
```

- **Bring-your-own**: the runtime is a stock Node + Postgres app; any host
that can run both works (Kubernetes, ECS, plain VM).

Expand Down
8 changes: 3 additions & 5 deletions docker-compose.build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ services:
build:
context: ./web-ui
dockerfile: Dockerfile
args:
# Build-arg: the rewrite target baked into the Next.js build. Use the
# in-network service name so the browser-loaded UI reaches the
# middleware through Docker's DNS, not the host's localhost.
MIDDLEWARE_URL: http://middleware:8080
# No MIDDLEWARE_URL build-arg: the /bot-api and /p proxies read it at
# request time (web-ui/app/_lib/middlewareProxy.ts), and the runtime
# value comes from the web-ui service env in docker-compose.yaml.
85 changes: 85 additions & 0 deletions fly/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# One-command Fly.io deploy of the omadia minimal core — the Fly
# counterpart to the repo-root render.yaml blueprint.
#
# ./fly/deploy.sh
#
# Provisions three Fly apps in YOUR Fly org (your account, your bill):
# - omadia-postgres-<suffix> pgvector/pgvector:pg17, private-only
# - omadia-middleware-<suffix> kernel API, persistent /data volume
# - omadia-web-ui-<suffix> admin UI, public entrypoint
#
# Secrets are generated here (VAULT_KEY, Postgres password); the LLM key
# is collected by the /setup wizard on first boot — nothing to paste.
#
# Prerequisites: flyctl installed and logged in (fly auth login), and
# openssl on PATH. Cost: three shared-cpu machines + two 1 GB volumes,
# roughly $10/month depending on usage.
#
# Overridable via environment:
# FLY_ORG Fly org slug (default: personal)
# FLY_REGION region for volumes/machines (default: fra —
# if you change it, also change primary_region in the
# three fly/*.fly.toml files)
# OMADIA_SUFFIX app-name suffix; Fly app names (default: random)
# are globally unique, hence one
#
# This is a one-shot installer, not an upgrade tool. To upgrade later:
# fly deploy --app <middleware-app> --config fly/middleware.fly.toml
# fly deploy --app <web-ui-app> --config fly/web-ui.fly.toml
# To tear everything down: fly apps destroy <each app>.

set -euo pipefail

FLY="$(command -v fly || command -v flyctl || true)"
if [ -z "$FLY" ]; then
echo "flyctl not found — install it first: https://fly.io/docs/flyctl/install/" >&2
exit 1
fi

ORG="${FLY_ORG:-personal}"
REGION="${FLY_REGION:-fra}"
SUFFIX="${OMADIA_SUFFIX:-$(openssl rand -hex 3)}"
PG_APP="omadia-postgres-${SUFFIX}"
MW_APP="omadia-middleware-${SUFFIX}"
UI_APP="omadia-web-ui-${SUFFIX}"

cd "$(dirname "$0")"

echo "==> Creating apps in org '${ORG}' (suffix: ${SUFFIX})"
"$FLY" apps create "$PG_APP" --org "$ORG"
"$FLY" apps create "$MW_APP" --org "$ORG"
"$FLY" apps create "$UI_APP" --org "$ORG"

echo "==> Creating volumes in ${REGION}"
"$FLY" volumes create omadia_pgdata --app "$PG_APP" --region "$REGION" --size 1 --yes
"$FLY" volumes create omadia_data --app "$MW_APP" --region "$REGION" --size 1 --yes

echo "==> Setting secrets"
PG_PASSWORD="$(openssl rand -hex 16)"
"$FLY" secrets set --app "$PG_APP" \
POSTGRES_PASSWORD="$PG_PASSWORD"
"$FLY" secrets set --app "$MW_APP" \
VAULT_KEY="$(openssl rand -base64 32)" \
DATABASE_URL="postgresql://omadia:${PG_PASSWORD}@${PG_APP}.internal:5432/omadia"
"$FLY" secrets set --app "$UI_APP" \
MIDDLEWARE_URL="http://${MW_APP}.internal:8080"

echo "==> Deploying Postgres (${PG_APP})"
"$FLY" deploy --app "$PG_APP" --config pg.fly.toml --ha=false

echo "==> Deploying middleware (${MW_APP}) — first boot runs migrations, allow a minute"
# If the middleware wins the race against initdb on the very first boot,
# its machine exits and Fly's restart policy retries until Postgres is up.
"$FLY" deploy --app "$MW_APP" --config middleware.fly.toml --ha=false

echo "==> Deploying admin UI (${UI_APP})"
"$FLY" deploy --app "$UI_APP" --config web-ui.fly.toml --ha=false

echo
echo "Done. Open https://${UI_APP}.fly.dev and finish the /setup wizard"
echo "(first admin + LLM key, stored encrypted in the vault)."
echo
echo "The middleware is public at https://${MW_APP}.fly.dev (needed for"
echo "channel webhooks). VAULT_KEY lives only as a Fly secret on ${MW_APP};"
echo "losing it makes vault entries unrecoverable."
50 changes: 50 additions & 0 deletions fly/middleware.fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Fly config — omadia middleware (kernel API + plugin runtime).
# Deployed by fly/deploy.sh (app name is passed via --app).
#
# Secrets set by deploy.sh: DATABASE_URL (points at the pg app over the
# private network), VAULT_KEY (generated; the GHCR image bakes
# NODE_ENV=production, which makes the key mandatory at boot — losing it
# makes vault entries unrecoverable). The LLM key is collected by the
# first-admin /setup wizard at runtime, so nothing else is required.
#
# DEV_ENDPOINTS_ENABLED stays unset on purpose: this app is publicly
# reachable, and the dev mounts expose raw KG + memory state without auth.

primary_region = "fra"

[build]
image = "ghcr.io/byte5ai/omadia-middleware:latest"

[env]
# Persist vault, installed.json, builder drafts.db, and uploaded plugin
# packages on the volume below instead of the image layer.
PLATFORM_DATA_DIR = "/data"

[mounts]
source = "omadia_data"
destination = "/data"

[http_service]
internal_port = 8080
force_https = true
# Keep the kernel always-on: cron routines and channel webhooks
# (Teams / Telegram) must not wait for a cold start.
auto_stop_machines = "off"
auto_start_machines = true
min_machines_running = 1

[[http_service.checks]]
interval = "10s"
timeout = "5s"
# First boot runs plugin-catalog load + KG migrations + bootstrap
# installs. Fly caps HTTP-check grace periods at 1 minute (longer
# values are lowered with a deploy warning), so this is the max —
# in practice the restart policy covers a slower first boot.
grace_period = "60s"
method = "GET"
path = "/health"

[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "1gb"
37 changes: 37 additions & 0 deletions fly/pg.fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Fly config — Postgres + pgvector for the omadia minimal core.
# Deployed by fly/deploy.sh (app name is passed via --app).
#
# Same image as docker-compose.yaml, so migrations behave identically.
# Fly's own Postgres offerings don't fit here: the unmanaged postgres-flex
# image ships without pgvector, and Managed Postgres gates the vector
# extension behind a dashboard toggle (and starts at a much higher price
# point). Running the pgvector image as a plain app keeps the stack
# scriptable end to end.
#
# No [http_service] / [[services]]: the database is reachable only over
# the org's private 6PN network at <app>.internal:5432 — never public.
# Fly takes daily snapshots of the volume; for managed backups/HA,
# migrate to Fly Managed Postgres and enable the vector extension there.

primary_region = "fra"

[build]
image = "pgvector/pgvector:pg17"

[env]
# Subdirectory below the mount: the official postgres image refuses to
# initdb into a directory that already contains lost+found (which every
# fresh Fly volume does).
PGDATA = "/var/lib/postgresql/data/pgdata"
POSTGRES_USER = "omadia"
POSTGRES_DB = "omadia"
# POSTGRES_PASSWORD comes in as a Fly secret (set by deploy.sh).

[mounts]
source = "omadia_pgdata"
destination = "/var/lib/postgresql/data"

[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "1gb"
25 changes: 25 additions & 0 deletions fly/web-ui.fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Fly config — omadia admin UI (Next.js).
# Deployed by fly/deploy.sh (app name is passed via --app).
#
# MIDDLEWARE_URL comes in as a Fly secret (set by deploy.sh) and points at
# the middleware app over the org's private 6PN network — the same
# topology as the compose stack's http://middleware:8080.

primary_region = "fra"

[build]
image = "ghcr.io/byte5ai/omadia-web-ui:latest"

[http_service]
internal_port = 3000
force_https = true
# The UI may scale to zero between visits; the proxy cold-starts it.
# The middleware underneath stays always-on.
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0

[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
80 changes: 80 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Render Blueprint — one-click deploy of the omadia minimal core
# (middleware + admin UI + Postgres/pgvector) into the deployer's OWN
# Render workspace. This file backs the "Deploy to Render" button:
#
# https://render.com/deploy?repo=https://github.com/byte5ai/omadia
#
# Mirrors docker-compose.yaml's minimal core; the optional overlays
# (MinIO, Kroki, Ollama) are not part of the blueprint.
#
# No secrets to paste: VAULT_KEY is generated by Render on first sync
# (the GHCR image bakes NODE_ENV=production, which makes the key
# mandatory at boot — see README "Deployment"), and the LLM key is
# collected by the first-admin /setup wizard at runtime and stored
# encrypted in the vault. DEV_ENDPOINTS_ENABLED stays unset: these
# services are publicly reachable, and the dev mounts expose raw KG +
# memory state without auth.
#
# Costs: the middleware needs a persistent disk, which requires paid
# instance types — `starter` for both services plus `basic-256mb`
# Postgres. Everything is pinned to one region (frankfurt) so the
# database connection string resolves over the private network.

services:
# --- Omadia middleware (TypeScript kernel + plugin runtime) ---------------
- type: web
name: omadia-middleware
runtime: image
image:
url: ghcr.io/byte5ai/omadia-middleware:latest
plan: starter
region: frankfurt
healthCheckPath: /health
envVars:
- key: DATABASE_URL
fromDatabase:
name: omadia-postgres
property: connectionString
# Vault master key. Generated once by Render; losing it makes vault
# entries unrecoverable, so never rotate it casually. Rotating LLM /
# plugin credentials happens inside the app (Admin → Runtime →
# Secrets), not by regenerating this key.
- key: VAULT_KEY
generateValue: true
# Persist vault, installed.json, builder drafts.db, and uploaded
# plugin packages on the disk below instead of the image layer.
- key: PLATFORM_DATA_DIR
value: /data
disk:
name: omadia-data
mountPath: /data
sizeGB: 1

# --- Admin UI (Next.js, talks to middleware over /bot-api) ----------------
- type: web
name: omadia-web-ui
runtime: image
image:
url: ghcr.io/byte5ai/omadia-web-ui:latest
plan: starter
region: frankfurt
envVars:
# Server-side base URL the admin UI uses to reach the middleware.
# RENDER_EXTERNAL_URL is the middleware's public https URL, injected
# by Render — a full URL with scheme, which MIDDLEWARE_URL requires.
- key: MIDDLEWARE_URL
fromService:
name: omadia-middleware
type: web
envVarKey: RENDER_EXTERNAL_URL

databases:
# --- Postgres + pgvector (knowledge graph, routines, verifier store) -----
# Migrations run automatically on the middleware's first boot, including
# CREATE EXTENSION IF NOT EXISTS vector (supported on Render Postgres).
- name: omadia-postgres
plan: basic-256mb
region: frankfurt
postgresMajorVersion: "17"
databaseName: omadia
user: omadia
11 changes: 5 additions & 6 deletions web-ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ COPY --from=deps /app/node_modules ./node_modules
COPY . .
# `output: 'standalone'` (next.config.ts) emits .next/standalone + .next/static
ENV NEXT_TELEMETRY_DISABLED=1
# Next.js bakes the /bot-api/* rewrite destination into routes-manifest.json
# at build time, so MIDDLEWARE_URL has to be known here — not at runtime.
# Override from fly.toml [build.args] or `fly deploy --build-arg` when moving
# the middleware onto a different internal hostname.
ARG MIDDLEWARE_URL=http://middleware:8080
ENV MIDDLEWARE_URL=${MIDDLEWARE_URL}
# MIDDLEWARE_URL is a pure RUNTIME setting: /bot-api/* and /p/* are proxied
# by route handlers that read it per request (app/_lib/middlewareProxy.ts),
# so the same image runs against any middleware host. Do not reintroduce a
# build ARG for it — that was the bug that pinned the published image to
# the compose hostname.
RUN npm run build

# --- runtime ----------------------------------------------------------------
Expand Down
Loading
Loading