Local demo first
make demo runs the same web UI against a real local stack at http://127.0.0.1:5183.
It keeps its own data across restarts; make demo-reset deletes only that demo’s data
and make verify-demo runs the browser checks. See
quivr-search/README.md for prerequisites, frontend
development and the optional shared password.
Hosted deployment
This deployment runs the realquivr-search UI and V2 core. One dedicated Railway project,
quivr-v2-demo, contains eight single-replica services. Only web is exposed publicly.
This is a single-node evaluation deployment, with the accepted Temporal dev server
and no high availability. Redeploying a volume-backed service can interrupt requests.
No Railway TCP proxies or public dependency domains are needed.
Webhook delivery and the RSS connector refuse private and internal addresses
(checked after DNS resolution, so Railway’s private network is unreachable
through them). The generated configuration never sets
delivery.allow_private_destinations or the rss pin’s allow_private_addresses;
those allowances exist for the local harness only.
The RSS connector is the first-party plugin plugins/rss,
built into the core image as /usr/local/bin/quivr-rss. core-entrypoint.py always
pins it and the worker always runs it on 127.0.0.1:9920, whatever QUIVR_DEMO_PLUGINS
says, so feed instances keep polling. It logs quivr-rss: serving connector.rss@… at start.
Credential key (optional)
The demo runs withoutQUIVR_CREDENTIAL_KEY. Ingestion, search and connectors that
need no credential, such as public RSS feeds, work normally. api and worker each log
credential deposits disabled once at startup. Credentialed connectors (Microsoft 365
mail, X lists, authenticated RSS) are then refused: creating one with a credential,
or rotating a credential, returns 503 credentials_unavailable.
To enable them later, set QUIVR_CREDENTIAL_KEY (32+ random bytes) to the SAME value
on both api and worker, then restart api and worker. No migration is needed. The one
side effect is that adding, changing or removing the key changes how connector
requests are fingerprinted for idempotency. A connector create sent before the change
and retried after it returns 409 idempotency_conflict instead of replaying. Keep the
value stable afterwards: changing or removing it also makes stored credentials
unreadable (access_error / credential_unreadable) until they are deposited again. provision.py generates this value by default.
Operator key (optional)
QUIVR_OPERATOR_KEY on api adds a second key with projections:rebuild and plugins:admin,
which the web app never gets. Use it from inside the deployment (railway ssh --service api,
port 8080) to rebuild a Corpus projection or to read the plugin registry (GET /v0/admin/plugins).
Ingestion plugin and the rebuild after THE-777
api and worker run the core.ingest plugin (127.0.0.1:9950, CONNECTORS in
core-entrypoint.py), which segments and embeds with TEI. After the first deploy with it,
rebuild each Corpus once with the operator key: POST /v0/corpora/{corpus_id}/rebuilds
with an idempotency_key, then poll the Operation. Until then search keeps working in
every mode and new articles are searchable by keyword at once; their vectors attach at the
rebuild, which re-embeds with the same model, so results do not change.
Connectors in the web app (optional)
The web app has a Sources view (THE-679, THE-732). By default the demo key cannot use it, so the view shows “Les connecteurs ne sont pas activés sur ce déploiement”. To enable it, setQUIVR_DEMO_CONNECTORS=1 on api and worker, then redeploy them. The
demo key then also gets connectors:read and connectors:write; it always has
changes:read, which the live health and the Veille feed use. Without QUIVR_CREDENTIAL_KEY only credential-free kinds, such as
public RSS, can be created, and the view says so. Unset the variable and redeploy to
turn it off again. Instances created meanwhile keep polling; pause or remove them
first from the view if they should stop.
Optional web variables for the Sources view (see quivr-search/README.md):
DEMO_FEED_SUGGESTIONS sets the one-click suggested feeds (JSON array of
{"title", "url"}), and DEMO_STATE_FILE keeps the list of removed sources on a
volume. Without a volume, removed sources come back as paused after a web restart.
Set the real feed list in the Railway variables, never in this repository.
X lists in webhook mode (docs/connectors/x-webhooks.md) need the API’s public
address: set QUIVR_PUBLIC_URL (for example https://<api domain>) on api and
worker. The API service then runs the x-list plugin beside itself to relay X’s
deliveries; without the variable, X lists only poll.
Keyword alerts and PDF text (optional)
The core image bakes in the first-party pluginsalerts,
which the Alertes tab needs, and pdf-text.
Set QUIVR_DEMO_PLUGINS=1 on api and worker and QUIVR_DEMO_DESTINATION_ID=demo-alerts-sink
on web, then redeploy api, worker and web. core-entrypoint.py then:
- pins both through the
pluginslist: pdf-text on127.0.0.1:9900(application/pdf), alerts on127.0.0.1:9910. The worker runs both as sidecar processes. The API also runs alerts, which it calls for alert previews. If any sidecar exits, its container stops and Railway restarts it; - offers described alerts (the pin’s
kinds) only whenTYPESAFE_API_KEYis set, with the same value on api and worker; only the alerts sidecars receive it. Without it, only keyword alerts can be created. With it, also setDEMO_DESCRIBED_ALERTS=trueon web so the Alertes tab offers them; - gives the demo key
monitoring:readandmonitoring:write; - declares the webhook destination
demo-alerts-sink, which every Subscription needs. The web app reads Matches through the API, so it points athttp://alerts-sink.invalid/, a reserved name that never resolves: deliveries fail inside the container, and the private-address refusal stays on. Its signing secret derives fromQUIVR_CURSOR_KEY.
plugins pinned with pdf-text@0.1.0 [normalizer] alerts@0.2.0 [subscription] and evaluators=1. Without a web DEMO_STATE_FILE volume, paused
alerts leave the list after a web restart; active ones are found again through the API.
Provision and deploy
Authenticaterailway login, then create/link a dedicated project in the intended
workspace. The provisioner refuses any project not named quivr-v2-demo or whose ID
does not match the explicit argument.
.scratch/railway/PROJECT_ID/secrets.json. Keep this file privately for
repeated provisioning; do not rotate the database or S3 password by rerunning with a
new file against an existing deployment. The web password is demo_password in that
file. It is a shared evaluation space, not per-user access.
The deployment helper connects pinned image sources or uploads Dockerfile services
from the repository root. It starts deployment but does not wait for readiness:
- postgres, seaweed, temporal, weaviate and tei; inspect deployment status/logs.
- api; its startup migration bootstraps schema, bucket and projection, then readiness.
- worker and web; verify readiness before publishing.
PORT=8081; internal API traffic uses port 8080. The worker
also probes on 8081. Only the web service uses its port 3000 for public traffic.
All core connections use fixed service DNS names within this project environment.
The tokenizer and model are downloaded and checksum-verified at build time from
accepted locks. TEI’s image contains the complete snapshot and HF_HUB_OFFLINE=1;
runtime startup does not download a model. Startup migration failure exits instead
of exposing a partially initialized API. Logs go to service stdout/stderr; runtime
configuration is generated privately in /tmp, never printed. The tokenizer stage
supports x86_64 only: on an arm64 machine, build the core image locally with
docker build --platform linux/amd64 -f deploy/railway/core.Dockerfile ..
Domain and HTTPS
*.up.railway.app target. thevibecompany.co currently has Cloudflare authoritative
nameservers, so changes in Vercel’s DNS UI alone do not publish those records.
Keep the facade DEMO_SECURE_COOKIE=true for HTTPS and preserve the original Host
header. TLS terminates at Railway; no public API key belongs in the browser bundle.
Verification and operations
- Inspect
railway service list --jsonand scoped logs for readiness/failures. - Open the public HTTPS URL, sign in, add a unique synthetic text, search in hybrid
and lexical modes, and read the unchanged source. Check the cookie is Secure and
HttpOnly. Anonymous
/demo/sessionmust return 401. - Save that Record/Version/Receipt identity, restart application and dependency services, then repeat search/source reads. Volumes must remain attached; never use service/volume deletion for this persistence check.
- Browser checks can target the public deployment with
QUIVR_DEMO_URLandQUIVR_DEMO_PASSWORDset in the test process environment. Do not publish traces containing passwords or submitted text. - Inspect actual memory/CPU/storage usage after startup and during an ingestion; idle estimates are not a monthly bill. Keep this single-replica demo on only while needed. Back up/export valuable content before deleting the demo project.
Primary references checked
- Railway private networking: environment-scoped service DNS, private HTTP connections, IPv4/IPv6 for new environments.
- Railway volumes: persistent mounts and deployment behavior.
- Railway domains: custom domain records and automatic TLS.
- Railway config as code: deployment configuration.
- Live CLI help and GraphQL schema introspection were used for the installed CLI;
Builderdoes not includeDOCKERFILE, so the provisioner setsdockerfilePathdirectly rather than supplying an invalid builder enum.
docs/dated/evidence/) once verified.