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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,11 @@ code; usage is metered durably in Postgres and enforced at the API edge.
Every push to `main` builds and publishes both service images to GHCR
(`ghcr.io/thefcan/ragdesk-api`, `ghcr.io/thefcan/ragdesk-ai`), so the stack runs
on any container host. A one-click **Render Blueprint** ([`render.yaml`](render.yaml))
provisions Postgres + pgvector, Redis, the API, the AI service and the web app on
the free tier — Postgres, Redis and the shared secrets are wired automatically.
provisions Redis, the API, the AI service and the web app on the free tier —
Redis and the shared secrets are wired automatically. Postgres + pgvector comes
from a free provider that does not expire (Neon, Supabase): you supply the
connection string, because Render's own free Postgres is deleted after 30 days
and takes the whole stack down with it.

See **[`docs/deploy.md`](docs/deploy.md)** for the full guide (Render, Fly.io,
Vercel, or your own `docker compose`).
Expand Down
4 changes: 2 additions & 2 deletions api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ require (
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/text v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
8 changes: 4 additions & 4 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,16 @@ golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
45 changes: 34 additions & 11 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,37 @@ docker pull ghcr.io/thefcan/ragdesk-ai:latest

## One-click deploy — Render Blueprint ($0)

[`render.yaml`](../render.yaml) describes the full stack: Postgres (with
pgvector), a Redis-compatible Key Value store, the API, the AI service and the
web app — all on Render's free tier.
[`render.yaml`](../render.yaml) describes a Redis-compatible Key Value store,
the API, the AI service and the web app — all on Render's free tier. Postgres
comes from elsewhere, for a reason worth knowing: **Render's free Postgres
expires after 30 days.** When it does, the instance stops and its internal
hostname stops resolving, so every service that migrates on boot crash-loops on
`no such host` until someone opens the logs. A free Postgres that does not
expire keeps the whole stack at $0 without that cliff.

1. **Fork** this repo.
2. In Render: **New → Blueprint**, connect your fork. Render reads `render.yaml`
and provisions every service.
3. `DATABASE_URL`, `REDIS_URL`, `JWT_SECRET` and the shared `AI_INTERNAL_TOKEN`
are wired **automatically**.
4. After the first deploy, set these URL values (they depend on the hostnames
2. **Create a free Postgres with pgvector** — [Neon](https://neon.tech) or
[Supabase](https://supabase.com) both work; the schema needs the `vector`
extension, and the migrations create it themselves on first boot. Copy the
connection string, and make sure it:
- ends with **`?sslmode=require`** — Render's own database URL did not need
it, a hosted one does, and without it the failure looks identical to a
missing database;
- uses the provider's **pooled** host if it offers one — free instances cap
connections below what several services reconnecting after a cold start
will ask for.
3. In Render: **New → Blueprint**, connect your fork. Render reads `render.yaml`
and provisions every service, prompting for the values marked `sync: false`
— `DATABASE_URL` among them.

> Paste the **same** connection string for **both** `ragdesk-ai` and
> `ragdesk-api`. They share one database and nothing verifies that they
> agree, so a typo in one produces a half-working stack. An environment
> group would carry the value once, but Render ignores `sync: false` inside
> a group, and a database URL is not something to commit to the repo.
4. `REDIS_URL`, `JWT_SECRET` and the shared `AI_INTERNAL_TOKEN` are still wired
**automatically**.
5. After the first deploy, set these URL values (they depend on the hostnames
Render assigns) and redeploy:

| Service | Variable | Value |
Expand All @@ -46,10 +67,12 @@ web app — all on Render's free tier.
| ragdesk-api | `WEB_BASE_URL` | `https://ragdesk-web.onrender.com` |
| ragdesk-web | `NEXT_PUBLIC_API_URL` | `https://ragdesk-api.onrender.com` |

5. Open the web URL and register. 🎉
6. Open the web URL and register. 🎉

> Render's free Postgres is time-limited and free services sleep when idle —
> fine for a demo. For always-on, bump the relevant plans.
> Render's free services sleep when idle, so the first request after a quiet
> spell waits on a cold start — fine for a demo. For always-on, bump the
> relevant plans. The database is no longer on that clock: it lives outside
> Render precisely so nothing expires out from under the stack.

## Embedding the widget

Expand Down
46 changes: 32 additions & 14 deletions render.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
# Render Blueprint — one-click deploy of the whole ragdesk stack on Render's
# free tier. Postgres (with pgvector), a Redis-compatible Key Value store, the
# Go API, the Python AI service and the Next.js web app.
# free tier: a Redis-compatible Key Value store, the Go API, the Python AI
# service and the Next.js web app. Postgres (with pgvector) lives OFF Render —
# see the env group below.
#
# DATABASE_URL, REDIS_URL and the shared secrets are wired automatically. A few
# URL values depend on the hostnames Render assigns, so they are marked
# REDIS_URL and the shared secrets are wired automatically. DATABASE_URL and a
# few URL values that depend on Render-assigned hostnames are marked
# `sync: false` — fill them in after the first deploy (see docs/deploy.md).
#
# The deploy defaults to the deterministic `fake` LLM providers so it runs at $0
# with no model server or API key. To get real answers, plug a hosted provider
# into the provider-agnostic AI layer (see docs/deploy.md).

databases:
- name: ragdesk-db
plan: free
postgresMajorVersion: "16"
# Postgres is deliberately NOT declared here. Render's free Postgres expires
# after 30 days: the instance stops, its internal hostname stops resolving, and
# every service that migrates on boot crash-loops on "no such host" until
# someone notices. A free Postgres from a provider that does not expire (Neon,
# Supabase — both ship pgvector, which this schema requires) keeps the stack at
# $0 without the 30-day cliff.
#
# DATABASE_URL is therefore a `sync: false` placeholder on each service that
# needs it, filled in from the dashboard. An environment group would express
# "one value, two consumers" better, but Render ignores `sync: false` inside
# an env var group, so a secret cannot travel that way — the group would sync
# with the variable simply absent.
#
# It is consequently declared TWICE, on ragdesk-ai and ragdesk-api, and the two
# must hold the identical value: they are the same database. A mismatch fails
# exactly like the outage this file exists to prevent.

services:
# Redis-compatible cache + ingestion queue.
Expand All @@ -33,10 +46,14 @@ services:
envVars:
- key: PORT
value: "8000"
# Your external Postgres. MUST carry `?sslmode=require` — Render's own
# database URL did not need it, a hosted one does, and without it the
# failure looks exactly like a missing database. Prefer the provider's
# *pooled* host: free instances cap connections below what these
# services ask for when they all reconnect after a cold start.
# Identical to ragdesk-api's copy below — same database.
- key: DATABASE_URL
fromDatabase:
name: ragdesk-db
property: connectionString
sync: false
- key: EMBEDDING_PROVIDER
value: fake # "gemini" for real hosted embeddings (free tier)
- key: CHAT_PROVIDER
Expand Down Expand Up @@ -65,10 +82,11 @@ services:
value: "8080"
- key: RAGDESK_ENV
value: production
# Must be byte-identical to ragdesk-ai's DATABASE_URL above — the two
# services read and write the same database, and nothing checks that
# they agree.
- key: DATABASE_URL
fromDatabase:
name: ragdesk-db
property: connectionString
sync: false
- key: REDIS_URL
fromService:
type: keyvalue
Expand Down
Loading