LapseIQ Installation Guide
LapseIQ is self-hosted software. Your contract data never leaves your infrastructure — there's no LapseIQ-operated cloud, no telemetry, no phone-home. ForgeRift LLC publishes the images and the installer; everything else runs on a server you control.
This guide takes you from "nothing yet" to a production instance — TLS, backups, and your team logging in — in roughly 30 minutes. No prior Linux experience is assumed; every command shows its expected output so you know whether it worked. Experienced operators can skip the hand-holding and jump straight to the one-line install.
Whichever path you take, you end up in the same place: a working LapseIQ
instance at https://lapseiq.example.com (or whatever domain you choose),
with your team's logins working and contracts ready to import.
How this guide is organized
Three parts. Most operators only need the first.
Get it running — pick the path that matches your experience:
- Fast track: one-line install — you already have a Linux box, a domain, and Docker. About five minutes.
- Full walkthrough — start with what you need first, then work through it step by step. ~30 minutes, no Linux background assumed.
- Manual install - run the published container images via Docker Compose, e.g. to pin a specific version.
Reference — look these up when a step points you at them:
Operate — running, maintaining, and fixing your instance:
Part 1: What you need before you start
A Linux server with internet access
LapseIQ runs as a Docker container on Linux. Host it on hardware you already own — a spare box, an on-prem VM, or a VPS you already run — so your data never leaves your own infrastructure. If you'd rather spin up a fresh cloud instance, any provider works; the ones below have one-click "Ubuntu 22 with Docker" templates so you can be SSH'd in within five minutes.
| Provider | Tier we'd pick |
|---|---|
| DigitalOcean | "Basic Droplet, Regular, 1 GB / 1 vCPU / 25 GB SSD" |
| Linode | "Nanode 1 GB" |
| Hetzner | "CX22" (EU only) |
| AWS EC2 | t4g.small with 20 GB gp3 EBS |
The first three offer one-click Ubuntu 22 images. Whichever you pick:
- OS: Ubuntu 22.04 LTS (or Debian 12). Both are well-tested.
- Specs: 1 GB RAM / 1 vCPU is plenty for the first hundred contracts; bump to 2 GB / 2 vCPU if you'll have heavy document uploads or several concurrent users.
- Disk: 25 GB minimum. Document uploads are the only thing that grows meaningfully; 100 GB is overkill for most teams.
- Region: pick one close to your team. LapseIQ doesn't replicate; the one region you pick is where the data lives.
After provisioning, you'll have an IP address (e.g. 206.189.200.29) and a
root password or SSH key. Save both.
A domain name pointing at the server
LapseIQ is meant to be reached at a real domain like lapseiq.example.com —
that's how TLS certificates work, and how your team will get used to logging
in. Two parts to this:
1. Buy a domain (or use one you already own). Any registrar works:
- Cloudflare Registrar — at-cost pricing, no markup.
- Namecheap — widely used, frequent deals.
- Porkbun — competitive pricing, friendly UI.
If you're hosting this for an existing company, you almost certainly already
own the domain. You'll add a subdomain (lapseiq.yourcompany.com).
2. Point that domain at your server. In your registrar's DNS panel, create an A record that maps your subdomain to the IP you got from the cloud provider above:
Type: A
Name: lapseiq (or whatever subdomain you want)
Value: 206.189.200.29 (your server's IP)
TTL: 300 (or "Automatic")
Save the change. DNS propagates within ~5 minutes for most registrars; some
take up to an hour. You can verify by running nslookup lapseiq.yourcompany.com
on your local machine — when it returns the right IP, you're ready.
About 30 minutes to set up
That's it. If you host on hardware you already run, there's nothing more to buy. If you use a cloud VM, you'll pay your provider their usual rate for a small instance plus a domain — LapseIQ itself has no subscription and no per-seat fees. The optional renewal-brief feature uses your own AI provider key, billed by that provider only when you use it.
Part 2: Step-by-step walkthrough
If you've never used SSH or Docker, follow this section line by line. Every command shows the expected output so you'll know whether it worked. If something doesn't match, jump to Troubleshooting.
Step 1: Connect to your server
From your laptop's terminal (macOS: Terminal.app; Windows: PowerShell or Windows Terminal; Linux: any terminal):
ssh root@206.189.200.29
Replace the IP with yours. The first connection will ask:
The authenticity of host '206.189.200.29 (206.189.200.29)' can't be established.
ED25519 key fingerprint is SHA256:abc...
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Type yes and press Enter. Then enter the root password your cloud provider
gave you (or, if you set up an SSH key during provisioning, it'll connect
without a password). You'll see something like:
Welcome to Ubuntu 22.04 LTS (GNU/Linux 5.15.0-x86_64)
root@lapseiq:~#
That # prompt means you're on the server. Everything from here runs there,
not on your laptop.
Step 2: Run the installer
Copy and paste this single line:
curl -fsSLO https://lapseiq.com/install.sh && less install.sh
This downloads the installer and pages through it so you can read what
it's about to do before running anything. Press q to quit less when
you've read enough. Then run:
bash install.sh
The installer will:
- Show the EULA and ask you to type
yesto accept. The full text lives athttps://lapseiq.com/eula. - Check for Docker. If Docker isn't installed, the installer offers to install it via Docker's official convenience script. Say yes; it takes about a minute.
- Ask you three questions:
- "Public domain" → enter
https://lapseiq.yourcompany.com(the domain you set up in Part 1). - "Admin email address" → your email. Used by the setup wizard.
- "Brevo API key for transactional email" → press Enter to skip for now. You can configure email later (Part 4).
- "Public domain" → enter
- Generate the secrets (POSTGRES_PASSWORD, JWT_SECRET, MASTER_KEY)
and write them to
.env. - Pull the LapseIQ images from GitHub Container Registry.
- Start the stack (database + server + client).
- Wait for the API to respond, then print the setup-wizard URL.
When it finishes, you'll see:
✓ API healthy at http://localhost:3001/api/health
LapseIQ is installed.
Next step: open the setup wizard and create your admin account:
https://lapseiq.yourcompany.com/setup
Operational hints:
- Logs: docker compose logs -f server
- Stop: docker compose down
- Update images: docker compose pull && docker compose up -d
- Backups: nightly pg_dump.gz lands in ./backups
- .env location: /root/lapseiq/.env (mode 600 — back this up off-box)
Step 3: Back up your .env RIGHT NOW
Don't skip this step. Do it before you do anything else with the installed instance.
The installer just generated three secrets. One of them — MASTER_KEY —
encrypts the API keys and 2FA secrets stored in your database. If you ever
lose MASTER_KEY, that data is gone forever. Not "we can recover it
with a support ticket" — gone. The encryption is genuine AES-256-GCM; there
is no backdoor.
Open the .env file and read the values:
cat /root/lapseiq/.env
Copy the entire contents into your team's password manager (1Password,
Bitwarden, etc.) under a note titled "LapseIQ — production .env". You're
specifically protecting MASTER_KEY, but back up the whole file because
losing JWT_SECRET would log out every user and losing POSTGRES_PASSWORD
would lock you out of the database.
Important: back up the .env somewhere DIFFERENT from where your
database backups go. If both end up in the same place and that place fails,
you've lost both halves.
Step 4: Set up TLS (HTTPS) with Caddy
Right now your install is running on HTTP only and is not safe for production use — passwords would travel across the network in cleartext. The fix is a reverse proxy that terminates TLS. We recommend Caddy because it auto-renews Let's Encrypt certificates for free with zero config.
Install Caddy:
apt update
apt install -y caddy
Create a Caddyfile that proxies traffic to LapseIQ:
cat > /etc/caddy/Caddyfile <<'EOF'
lapseiq.yourcompany.com {
reverse_proxy localhost:3001
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
Replace lapseiq.yourcompany.com with your actual domain. Then:
systemctl reload caddy
Caddy will contact Let's Encrypt, prove ownership of your domain (using the DNS record you set up in Part 1), and obtain a TLS certificate. This takes about 30 seconds on first run. If it fails, see Troubleshooting → Caddy / TLS issues.
Now update your LapseIQ install to know it's behind a proxy. Edit .env:
nano /root/lapseiq/.env
Find the line TRUST_PROXY=true and confirm it's there (install.sh wrote
it). For a Cloudflare-fronted setup, replace it with the CIDR allowlist
form — see TLS termination in the reference section.
Restart LapseIQ so it picks up the change:
cd /root/lapseiq
docker compose up -d
Open https://lapseiq.yourcompany.com in your browser. You should see a
green padlock and the LapseIQ login screen.
Step 5: Run the setup wizard
The first visit lands on /setup. Walk through the four steps:
- Account info — your company name (purely cosmetic, shows in the UI).
- Admin account — your name, the email you provided to install.sh, and a password. This is your initial admin user.
- Email — choose "Skip for now" or paste a Brevo API key (see Optional environment → Email in the reference section if you want to set this up now).
- AI — choose "Skip for now", paste a Cloudflare Workers AI key (recommended for new operators — generous free tier, no credit-card account needed), or pick a different provider from the dropdown and paste its key. Skipping leaves LapseIQ as a pure manual-entry tool with no LLM calls. You can change provider or rotate keys later from Settings -> AI & Extraction.
After you submit, you're logged in as the admin and you can start creating contracts, inviting team members, and importing your first CSV.
Step 6: Run the pre-launch smoke test
If you configured an AI provider in Step 5, run the smoke test once to confirm every AI integration works against your live key. From your SSH session:
cd /root/lapseiq
docker compose exec server node scripts/ai-smoke-test.js
The script exercises all four AI integrations (PDF ingest, signature
extraction, renewal brief, news scanner) and prints PASS / FAIL per
integration. Any FAIL is a launch blocker. It makes a handful of small AI
calls billed by your own provider. Exits with code 0 on full pass, 1 on any
failure, 2 if no API key is configured.
If you skipped AI in Step 5, skip this step too — there's nothing to test.
Step 7: Set up the first backup
Backups already run nightly to ./backups at 02:00 server time. Verify
that path is writeable and confirm a manual backup right now:
cd /root/lapseiq
docker compose exec db pg_dump -U lapseiq lapseiq | gzip > backups/pre-launch.sql.gz
ls -lh backups/
You should see your pre-launch.sql.gz listed. Now copy that file off the
server (S3, your laptop, anywhere except the same VM):
# From your LAPTOP, not the server:
scp root@206.189.200.29:/root/lapseiq/backups/pre-launch.sql.gz ./
If you want backups to ship automatically to S3, see Optional environment → Backups in the reference section.
You're done. Invite your team via Settings → Users, import contracts via the CSV import button on the Contracts page, and you're running.
One-line install
For readers who already have a Linux box, a domain, and Docker familiarity. This collapses Part 2 to a single command:
curl -fsSLO https://lapseiq.com/install.sh
less install.sh # read what it's about to do
bash install.sh
The two-step curl + less + bash pattern is recommended over a piped
curl … | bash: same end result, but you get to read the script before
running it. If you've already inspected the script and trust this project,
the one-liner is:
curl -fsSL https://lapseiq.com/install.sh | bash
Either way, the script:
- Detects Docker (installs it on Ubuntu/Debian via Docker's own convenience script if missing; refuses on macOS and tells you to install Docker Desktop).
- Prompts for public domain, admin email, and an optional Brevo
API key. Skipping Brevo leaves email in
EMAIL_MOCK=truemode (emails log to stdout) — useful for kicking the tires. - Generates
POSTGRES_PASSWORD,JWT_SECRET, andMASTER_KEYwithopenssl rand. Back up the resulting.envimmediately —MASTER_KEYcannot be regenerated. - Pulls
ghcr.io/forgerift/lapseiq-server:latestandghcr.io/forgerift/lapseiq-client:latest. - Brings up the stack with
docker compose up -d, polls/api/healthuntil it's green, and prints the setup-wizard URL.
After install, finish with the TLS + setup wizard + smoke test + backup steps from Part 2.
Non-interactive install (CI / IaC)
The installer requires interactive EULA acceptance by default. To bypass for automation:
LAPSEIQ_ACCEPT_EULA=1 bash install.sh
# or
bash install.sh --yes
Either path is logged to .lapseiq-eula-accepted in the install directory.
Both are an affirmation of the EULA at https://lapseiq.com/eula.
Windows operators
If you run Docker Desktop on Windows, use the PowerShell installer instead
of install.sh. Same prompts, same end state, just pwsh-native:
iwr -useb https://lapseiq.com/install.ps1 -OutFile install.ps1
notepad install.ps1 # read what it's about to do
pwsh -ExecutionPolicy Bypass -File .\install.ps1
Or, if you trust this project:
iex (iwr -useb https://lapseiq.com/install.ps1).Content
The PowerShell installer mirrors install.sh's behaviour: detects Docker
Desktop, prompts for domain / admin email / optional Brevo API key,
generates secrets via .NET's RandomNumberGenerator, writes .env, pulls
the GHCR images, brings the stack up, polls /api/health, and prints the
setup-wizard URL. The same LAPSEIQ_ACCEPT_EULA=1 / -Yes non-interactive
flags are supported.
Verifying installer integrity
Each installer ships with a companion .sha256 file. Before running
either script, verify it matches:
# bash / Linux / macOS
curl -fsSLO https://lapseiq.com/install.sh
curl -fsSLO https://lapseiq.com/install.sh.sha256
sha256sum -c install.sh.sha256
bash install.sh
# PowerShell / Windows
iwr -useb https://lapseiq.com/install.ps1 -OutFile install.ps1
iwr -useb https://lapseiq.com/install.ps1.sha256 -OutFile install.ps1.sha256
$expected = ((Get-Content install.ps1.sha256) -split '\s+')[0]
$actual = (Get-FileHash install.ps1 -Algorithm SHA256).Hash.ToLower()
if ($expected -ne $actual) { throw "checksum MISMATCH" } else { "checksum OK" }
pwsh -ExecutionPolicy Bypass -File .\install.ps1
The checksums are committed in the repo alongside the scripts and
regenerated via scripts/generate-checksums.sh whenever the scripts
change. A mismatch means either the script was tampered with in transit
or you have a stale .sha256 file — re-download both.
Manual install with published images
If you'd rather run the stack by hand instead of the one-line installer - usefulnwhen you want to pin a specific version or wire LapseIQ into your own Composenfile - pull the published images:nnbashn# Download the published Compose file from lapseiq.comncurl -fsSLO https://lapseiq.com/docker-compose.ghcr.ymln# Create a .env next to it with the required values (see "Required environment" below)ndocker compose -f docker-compose.ghcr.yml up -dn
The images pull in about a minute. When docker compose ps shows
all three services (db, server, client) as running / healthy,
visit your server's URL — the first visit lands on /setup.
Required environment
Every value below must be present in .env before the first
docker compose up. The compose file uses ${VAR:?...} guards on the
must-set secrets, so the stack refuses to start on a missing value rather
than silently picking a weak default.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Postgres connection string. When using the bundled db service this is built from POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB. |
POSTGRES_PASSWORD |
Bundled-Postgres credential. Generate a long random value — there is no recovery path if a public-internet Postgres is brute-forced. |
POSTGRES_USER |
Defaults to lapseiq. Override only if you need to. |
POSTGRES_DB |
Defaults to lapseiq. |
JWT_SECRET |
Signs JWT access tokens. Must be ≥ 32 characters. The startup check refuses common defaults (changeme, secret, etc.). |
MASTER_KEY |
Encrypts API keys, TOTP secrets, and (when ENCRYPT_DOCS=true) document content at rest. Critical — see MASTER_KEY backup below. |
CLIENT_URL |
The public URL the SPA is served from. Required in production. Used for CORS origin and email link bases. |
Every other value in server/.env.example is optional or has a sane default.
Generate the secret values with:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" # MASTER_KEY
node -e "console.log(require('crypto').randomBytes(48).toString('base64'))" # JWT_SECRET
node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))" # POSTGRES_PASSWORD
If you ran install.sh, these were generated for you and live in .env
already.
MASTER_KEY backup
The most important paragraph in this document.
MASTER_KEY is the root of all symmetric encryption inside LapseIQ. It
encrypts:
- DB-stored API credentials (AI provider keys, S3 keys, cloud-marketplace connector credentials)
- Two-factor authentication TOTP secrets
- Uploaded document content, when
ENCRYPT_DOCS=true
If you lose MASTER_KEY, none of that data is recoverable. Not by ForgeRift
LLC, not by any tool — the encryption is honest AES-256-GCM with HKDF-derived
per-record keys. There is no key escrow.
Two non-negotiables:
Back it up. Put the value in your team password manager (1Password, Bitwarden, AWS Secrets Manager, Hashicorp Vault — pick one). Do this before the first
docker compose up. If you raninstall.sh, do it the moment the installer finishes.Back it up somewhere different from your database backup. If a single storage location loses both the encrypted DB and the key, you have lost everything. Standard practice is "DB backups go to S3 bucket A,
MASTER_KEYlives in your password manager separately."
MASTER_KEY is rotated by re-encrypting in place, which is currently a
manual operation — plan to keep the same value for the lifetime of the
instance unless you have a specific compromise event.
Volume mounts
LapseIQ keeps three categories of state outside the application image so
they survive image rebuilds and container recreations. The default
docker-compose.yml wires these up; if you customize compose, preserve all
three.
| Mount | Purpose |
|---|---|
postgres_data (named volume) |
The database itself. Backed by Docker on the host filesystem. |
./uploads (bind mount) |
Local document storage when STORAGE_DEST=local (default). When STORAGE_DEST=s3, this directory is unused. |
./backups (bind mount) |
Nightly pg_dump output when BACKUP_DEST=local (default). |
The bind-mounted directories live next to your compose file. On a fresh clone they are created automatically when compose first starts.
TLS termination
LapseIQ does not terminate TLS itself. You must front the app with a reverse proxy that does — running it on plain HTTP outside of localhost is not safe.
The recommended setup is Caddy (auto-renews Let's Encrypt certificates with zero config). Minimal Caddyfile:
lapseiq.example.com {
reverse_proxy localhost:3001
}
A hardened production Caddyfile with security headers is shown in Step 4.
nginx works equally well. A worked nginx example with the right headers
for LapseIQ lives in docs/examples/nginx in the repo.
If you put a reverse proxy in front of LapseIQ, set TRUST_PROXY in
.env so Express resolves the real client IP from X-Forwarded-For.
Without this, every audit-log row records the proxy's IP and per-IP rate
limits collapse to a single bucket.
Three forms are supported, in order of preference:
A. CIDR allowlist (recommended for production). Comma-separated list
of networks Express trusts as legitimate proxy hops. Forged
X-Forwarded-For entries from outside the allowlist are structurally
ignored, blocking IP spoofing through a proxy chain.
For Cloudflare-fronted deployments, run the helper:
bash scripts/get-trust-proxy-cidrs.sh
It prints a paste-ready single line containing localhost, the standard
Docker bridge subnet, and the canonical Cloudflare v4+v6 ranges. Paste the
output into .env:
TRUST_PROXY=127.0.0.1,172.16.0.0/12,173.245.48.0/20,...,2400:cb00::/32,...
Re-run the helper periodically (cron weekly is fine) and update .env
when Cloudflare expands their ranges. The exact ranges have been stable
for years.
B. Legacy boolean. TRUST_PROXY=true is kept for backward compat. It
makes Express trust ONE upstream hop. Acceptable when your firewall
absolutely guarantees nothing can reach the reverse proxy bypassing the
front end (e.g. the proxy is on the same host as the app and only that
host has port 80/443 open).
C. Multi-hop integer. For a chain like CloudFront → ALB → server,
TRUST_PROXY=2 tells Express to trust two upstream hops. Pin a specific
number rather than enabling broad trust.
Security caveat. Never enable
TRUST_PROXY=trueon a directly- internet-exposed instance. Any client can spoofX-Forwarded-Forheaders on a server they reach without a trusted proxy in between, which would let them bypass per-IP rate limits and inject fake IPs into your audit log.
Optional environment
Only set the values you actually need. Everything below is opt-in.
Email (Brevo)
Required in production for password reset emails, user invites, and (if
you turn it on) feedback submissions. Set EMAIL_MOCK=true to log emails
to stdout instead of sending — useful for local development.
For real email, sign up at https://brevo.com, verify your sending domain (SPF + DKIM records, takes ~30 minutes including DNS propagation), generate an API key from Brevo's dashboard (Settings → SMTP & API → API Keys), and set:
BREVO_API_KEY=xkeysib-xxxxxxxxxxxxxxxxxxxxxxxx-yyyyyyyyyyyyyyyyyyyyyyyyyy
EMAIL_FROM="LapseIQ <noreply@yourdomain.com>"
SUPPORT_EMAIL=you@yourdomain.com
Older versions of LapseIQ used Resend; the env var name and key format are different. If you set
RESEND_API_KEYinstead ofBREVO_API_KEY, the server logs a one-time deprecation warning at startup and email sending is skipped. Rename toBREVO_API_KEYand use a Brevo key.
AI provider + fallback cascade
Required for contract field extraction and renewal-brief generation. Set
AI_ENABLED=false to run LapseIQ as a pure manual-entry tool with no LLM
calls. To enable AI, pick a primary provider:
AI_ENABLED=true
AI_PROVIDER=cloudflare # primary (default + recommended)
CF_WORKERS_AI_API_KEY=... # Cloudflare account API token
Supported primary providers: cloudflare (default), anthropic,
openai, azure_openai, gemini. Each provider has its own credential
env var: CF_WORKERS_AI_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY,
AZURE_OPENAI_API_KEY, GEMINI_API_KEY. The legacy generic
AI_API_KEY is still honored for Anthropic-only deployments.
Fallback cascade. When the primary fails (rate limit, quota exhausted, network blip, model deprecation), LapseIQ retries the same request against Hugging Face Inference, then Groq. Each fallback is free-tier-friendly and gated by its own per-action budget so a hostile visitor can't drain quota indefinitely. To enable the fallbacks, supply their keys:
HUGGINGFACE_API_KEY=hf_...
GROQ_API_KEY=gsk_...
The cascade fires automatically — no config flag to flip. If you omit the fallback keys, the cascade simply stops at the primary; an outage of the primary then surfaces as a brief-generation error rather than a transparent recovery.
You can also leave AI_PROVIDER and all provider keys blank and save
the credentials inside the app via Settings -> AI & Extraction — they
are encrypted with MASTER_KEY before being persisted. DB-stored keys
satisfy the same provider-aware startup gate that env-stored keys do.
Web-search enrichment (Tavily, optional)
Set TAVILY_API_KEY to enable Tavily web-search enrichment on the renewal
brief's Market section. Per-template domain allowlists keep results bounded
to authoritative sources (FCC for telecom, FRED for lease, EIA for
utilities, etc.). When the key is omitted, briefs still generate — the
no-reference fallback fires and the model leans on its training-time
knowledge for the Market section.
# Optional — omit to disable web-search enrichment
TAVILY_API_KEY=tvly-...
The renewal brief is gated by a per-account toggle (Settings → AI → "Enable AI Renewal Brief"), default OFF on self-host. Admins flip it on per account.
AI daily caps
Self-host runs uncapped by default — operators bring their own AI key, credit spend is the operator's concern. On a shared instance (e.g. internal demo, public sandbox), cap per user with:
# Uniform fallback
# AI_DAILY_CAP_PER_USER=
# Per-action overrides
# AI_DAILY_CAP_PER_USER_EXTRACT= (PDF ingest + signature reading SHARED)
# AI_DAILY_CAP_PER_USER_ASK= (Ask LapseIQ assistant)
# AI_DAILY_CAP_PER_USER_BRIEF= (Renewal brief generation)
# AI_DAILY_CAP_PER_USER_BRIEF_SEARCH= (Tavily web search per brief)
DEMO_MODE=true applies sensible default caps (extract=2, ask=6, brief=1,
brief_search=1) without any explicit env override.
S3-compatible storage
Default is local filesystem. Switch to S3 (or Backblaze B2 / Wasabi /
MinIO / Cloudflare R2) by setting STORAGE_DEST=s3 plus STORAGE_S3_*
credentials:
STORAGE_DEST=s3
STORAGE_S3_BUCKET=lapseiq-uploads-yourcompany
STORAGE_S3_REGION=us-east-1
STORAGE_S3_ENDPOINT=https://s3.amazonaws.com
STORAGE_S3_ACCESS_KEY_ID=AKIA...
STORAGE_S3_SECRET_ACCESS_KEY=...
The presigned-URL TTL defaults to 5 minutes; configurable via
STORAGE_S3_URL_TTL_SECONDS=300.
Document encryption
Set ENCRYPT_DOCS=true to encrypt every uploaded document with AES-256-GCM
at rest. Requires MASTER_KEY to be set (which it always should be).
Existing unencrypted uploads remain readable — only new uploads are
encrypted.
Backups
Default is a nightly local pg_dump at 02:00 server time into ./backups.
To ship backups to S3 instead, set:
BACKUP_DEST=s3
BACKUP_S3_BUCKET=lapseiq-backups-yourcompany
BACKUP_S3_REGION=us-east-1
BACKUP_S3_ENDPOINT=https://s3.amazonaws.com
BACKUP_S3_ACCESS_KEY_ID=AKIA...
BACKUP_S3_SECRET_ACCESS_KEY=...
BACKUP_DEST=both writes to both targets.
A note on cron scheduling and timezones
LapseIQ's nightly jobs (backups, audit-log prune, demo reset, alert
digest, news scan) are scheduled with node-cron against server-local
time. On a UTC droplet — the default for DigitalOcean, Hetzner,
Linode, and most cloud providers — that means the published times
(02:00 backups, 03:00 audit-log prune, 03:30 demo reset, 07:00 alert
engine) all fire at the same UTC clock-time around the world.
If your server's timezone is something other than UTC (you set it
during provisioning, or your provider defaults to a regional zone),
those jobs fire at the equivalent local clock-time. A server set to
America/Chicago will run the 02:00 backup at 02:00 CST, not at
02:00 UTC.
Two options:
Recommended: keep the server on UTC. This is the default for essentially every cloud provider, and it makes log timestamps easier to correlate across regions. Verify with
timedatectlon Ubuntu/Debian.Set the timezone explicitly on the server if your team needs logs in local time. The cron times will then fire in that local time. Example:
timedatectl set-timezone America/Chicagoflips the server clock; LapseIQ picks it up on next restart.
There is currently no env var to pin individual crons to UTC
independent of the server clock. If you need that, file an issue —
it's a small addition to index.js cron definitions but we haven't
shipped it yet.
Audit log retention
ACTIVITY_LOG_RETENTION_DAYS=365 (default) trims the audit log nightly at
03:00. Lower this for high-volume tenants where the audit table is
dominating pg_dump time. BACKUP_LOG_RETENTION_DAYS=180 (default) does
the same for the backup-history table.
Pre-launch smoke test
Before you open a freshly-configured instance to users — and after every AI provider, model, or SDK change — run:
docker compose exec server node scripts/ai-smoke-test.js
(Or, if you cloned the source: node server/scripts/ai-smoke-test.js
from the repo root.)
The script exercises every AI integration — PDF ingest extraction,
signature image extraction, renewal-brief generation, and news scanner
classification — against your live primary provider (and the fallback
cascade if the primary fails) and prints PASS / FAIL per integration.
Any FAIL is a launch blocker. It makes a handful of small AI calls,
billed by your own provider (free on Cloudflare Workers AI's free tier).
Exits 0 on full pass, 1 on any failure, 2 if no API key is configured
(so CI can tell "not configured" apart from "broken").
Feedback feature gate
LapseIQ ships with an in-product feedback button that, when enabled,
emails submissions to the address in SUPPORT_EMAIL via your Brevo
account.
Default behaviour:
- Self-hosted production (
DEMO_MODE=falseor unset): feedback is OFF by default. SetFEEDBACK_ENABLED=trueandSUPPORT_EMAIL=...in your.envto enable. This matches the EULA S5 carve-out, which says the feature can be disabled by settingFEEDBACK_ENABLED=false. - Demo deployment (
DEMO_MODE=true): feedback is ON by default so the demo operator receives input from visitors. Demo visitors are notified inline on the feedback button and via the Demo Sandbox Notice page.
Demo mode
DEMO_MODE=true puts the running instance into a constrained sandbox
suitable for public sales demos and recorded walkthroughs. When set:
EMAIL_MOCKis forcedtrue— no real emails ever sentAI_ENABLEDis forcedtrue(with Haiku-only model override) so the demo can showcase AI features without runaway costREGISTRATION_OPENis forcedtrue— visitors can self-serve a sandbox- Per-action AI quotas are pinned (extract=2/day, ask=6/day, brief=1/day,
brief_search=1/day) regardless of
AI_DAILY_CAP_PER_USER_*overrides - A nightly cron at 03:30 wipes user-generated data and re-runs
scripts/seed-demo.js
To start a demo instance from scratch:
echo "DEMO_MODE=true" >> .env
docker compose up -d
docker compose exec server node scripts/seed-demo.js
The seed creates four canned accounts:
admin@demo.local / Admin1234!
manager@demo.local / Manager1234!
viewer@demo.local / Viewer1234!
consultant@demo.local / Consultant1234!
Demo mode persists across restarts but resets data nightly. Banner copy at the top of the SPA reminds visitors not to enter real data. Admins can trigger an immediate reset from Settings → Account Data → Reset demo instance.
Upgrades
LapseIQ migrations run automatically on container startup
(prisma migrate deploy is part of the server CMD). The standard upgrade
path is:
# Always take a backup first
cd /root/lapseiq
docker compose exec db pg_dump -U lapseiq lapseiq | gzip > backups/pre-upgrade-$(date +%F).sql.gz
# If you installed via install.sh / GHCR images:
docker compose pull
docker compose up -d
# If you cloned and build from source:
git pull
docker compose up --build -d
Migrations are forward-only and idempotent — running an upgraded image against an already-current database is a no-op. Prisma's deploy command takes a row-level advisory lock, so two replicas starting simultaneously will not double-run migrations.
Always take a database backup before any upgrade. A manual
docker compose exec db pg_dump -U lapseiq lapseiq | gzip > pre-upgrade.sql.gz
is enough; the nightly cron also writes one to ./backups automatically
if the timing happens to be convenient.
Troubleshooting
Common failures with the literal error you'll see and what to do about each.
Install-time issues
Docker not found. Install Docker Desktop from https://docker.com/...
You're on macOS and Docker isn't installed. Install Docker Desktop, start
it, and re-run bash install.sh. The installer doesn't auto-install
Docker on macOS because Docker Desktop has license terms that should be
read.
Need sudo (or root) to install Docker. Re-run as root or install sudo first.
You're on Linux as a non-root user without sudo available. Either
re-run as root (su - then re-run install.sh) or have your sysadmin
install sudo.
Docker Compose v2 plugin not found. You have the old
docker-compose script but not the new docker compose v2 subcommand
(note the space). On Ubuntu/Debian:
apt install docker-compose-plugin. On macOS, Docker Desktop bundles it.
Admin email is required. You pressed Enter at the admin-email
prompt without typing anything. Re-run install.sh and provide a valid
email — it's used by the setup wizard to identify your initial admin
user.
openssl not found. Rare. Install with apt install openssl (Ubuntu/
Debian) or brew install openssl (macOS). The installer uses openssl to
generate JWT_SECRET, MASTER_KEY, and POSTGRES_PASSWORD.
API didn't respond at http://localhost:3001/api/health within 30s.
The server container didn't come up cleanly. Run
docker compose logs server to see what happened. Common causes:
MASTER_KEYmissing or malformed (must base64-decode to 32 bytes).DATABASE_URLpoints at a DB that doesn't exist or won't accept connections.- Port 3001 is already in use by another process.
Runtime / 503 / health-check issues
docker compose up exits with POSTGRES_PASSWORD must be set. You
have not populated .env yet. Copy from .env.example and fill the
required values listed above. If you ran install.sh, .env should
already exist — check that you're in the right directory (cd /root/lapseiq).
/api/health returns 503 with needsSetup: true. This is normal on
a fresh instance — visit /setup in your browser to create the initial
admin. The setup gate releases automatically once the wizard completes.
/api/health returns 503 with error: "Database unreachable.". The
Postgres container has not finished initialization, or DATABASE_URL is
wrong. Check docker compose logs db. If the DB is stuck, restart it:
docker compose restart db. If the issue persists, check that
POSTGRES_PASSWORD in .env matches what's encoded in DATABASE_URL —
install.sh writes them consistently, but hand-edited .envs sometimes
drift.
MASTER_KEY must decode to exactly 32 bytes at startup. Your value
is not a valid base64-encoded 32-byte string. Regenerate with the command
in the Required environment section. If this
is an existing install (not a fresh one), DO NOT regenerate — your data
is encrypted with the OLD key and a new key makes it unreadable.
Recover the original from your password manager backup. If you have no
backup, restore the entire .env from a prior database backup point in
time.
JWT_SECRET is too short or JWT_SECRET is a known-weak value. The
startup check refuses common defaults (changeme, secret, etc.) and
anything shorter than 32 characters. Generate a strong one with the
command in Required environment.
Auth / cookie / refresh-token issues
Refresh tokens immediately fail with Invalid refresh token. Almost
always a sign that the SPA is calling the API across a domain mismatch
that breaks the cookie or Authorization header. Verify CLIENT_URL
matches the URL in your browser's address bar exactly (scheme, host, and
port all included). If you're testing locally on
http://localhost:5173, CLIENT_URL must be exactly
http://localhost:5173 — not http://127.0.0.1:5173, not
https://localhost.
"Too many attempts" on login. The per-IP auth rate limiter kicked
in after several failed attempts in a row. Wait 15 minutes, or restart
the server container to clear the in-memory state. If you keep tripping
this on legitimate logins, you probably need TRUST_PROXY configured
(see next item).
Audit log shows the same IP for every event. You enabled
TRUST_PROXY without a reverse proxy, OR your reverse proxy is not
setting X-Forwarded-For, OR your TRUST_PROXY value doesn't include
the proxy's network. For a Cloudflare-fronted setup, run
bash scripts/get-trust-proxy-cidrs.sh and paste the output into
.env.
Caddy / TLS issues
Failed to obtain certificate: timeout when Caddy first starts up.
Almost always a DNS issue: Let's Encrypt can't verify ownership of
lapseiq.yourcompany.com because DNS hasn't propagated yet, or your
A record points at the wrong IP. Verify with
nslookup lapseiq.yourcompany.com from your laptop — it should return
your server's IP within ~5 minutes of creating the A record.
Browser shows Your connection is not private even after Caddy
started. Caddy might still be obtaining the cert. Wait 30 seconds and
refresh. If it persists, journalctl -u caddy -n 50 shows what Caddy is
doing.
Cloudflare in front of Caddy → infinite redirect loop. Cloudflare's default "Flexible" SSL mode talks HTTP to your origin, but Caddy is set up for HTTPS-only. Either:
- Set Cloudflare's SSL/TLS mode to "Full (strict)" — recommended.
- OR remove TLS from Caddy and let Cloudflare handle it (not recommended — your traffic to the origin is unencrypted).
Docker / disk / system issues
no space left on device during docker compose pull. Old images
are taking up disk. Clean up: docker system prune -a (this removes
unused images, containers, and networks — confirm with y). Be sure
you actually want this; running it while LapseIQ is up is safe, but it
will re-pull on the next start.
Container restarts in a loop. docker compose ps shows status
Restarting. Pull the logs: docker compose logs server for the API,
docker compose logs db for Postgres, docker compose logs client for
the SPA. The error message at the bottom is what you need.
Server container is up but /api/health times out for minutes. The
Prisma migration step on startup is running against a large database
that doesn't have prior migrations applied. Wait — prisma migrate deploy is idempotent and takes an advisory lock so two starts won't
race. Logs will show Applied migration X per file as it works through
them.
Performance issues
Pages load slowly under light load. Likely the bundled Postgres
container running on a 1 GB RAM VM is paging to disk. Bump the VM to 2
GB RAM and docker compose up -d. If you have heavy document uploads
and many concurrent users, move Postgres to a managed DB (DigitalOcean
Managed Database, AWS RDS, etc.) and point DATABASE_URL at it — the
bundled db service is fine for the first 100 contracts but isn't
tuned for production scale.
Where to go from here
- Get LapseIQ. LapseIQ ships as prebuilt container images - use then one-line install or then manual install. The source repository is private.n- Issue tracker. File bugs and feature requests by email ton
support@lapseiq.com. - Roadmap.
ROADMAP.mdin the repo root tracks the product surface. - Security disclosures. Email
support@lapseiq.com(encrypted via the public key on the security page, if you have one). Please don't disclose security issues via public GitHub issues. - Commercial questions. ForgeRift LLC operates the project. Contact details are on https://lapseiq.com.