OpenLOS means Open Letter of Support.
OpenLOS is an open-source, local-first tool for creating a trustworthy, frozen, human-endorsed professional artifact. A candidate writes a reusable support statement, freezes an immutable version, asks supporters to endorse that exact version, and exports the result with visible provenance.
OpenLOS is not a social network, recruiter product, analytics platform, blockchain proof system, or legal e-signature tool.
OpenLOS helps a person create a professional letter of support that can be reviewed and endorsed by people who know their work.
The core artifact contains:
- candidate name
- letter title
- frozen content snapshot
- frozen version number and timestamp
- SHA-256 content hash
- supporter endorsements tied to that version
- active or expired endorsement states
- provenance metadata
Professional recommendations are valuable but repetitive. Supporters often believe in someone but cannot write a custom letter every time. OpenLOS keeps the burden low without hiding authorship: the candidate writes the statement, freezes it, and supporters decide whether they stand behind that exact frozen version.
OpenLOS is designed to run locally or on infrastructure you control.
- Next.js and React provide the app UI.
- Prisma writes to SQLite.
- Drafts, frozen versions, requests, endorsements, exports, and audit events persist locally.
- SMTP is optional.
- External sharing is controlled by
APP_BASE_URL. - Docker Compose can persist SQLite in a named volume.
No external service is required for first boot. Email and tunnels are user-configured options.
OpenLOS supports multiple local letter files. The hierarchy is:
Letter
-> active editable Draft
-> Frozen Version v1
-> Frozen Version v2
-> Frozen Version v3
A letter has one active editable draft and can have many immutable frozen versions. Creating a new letter from Create New creates a separate Letter record. Saving a draft updates only that letter's LetterDraft. Freezing creates a new LetterVersion for the same letter and never overwrites prior frozen versions.
Use Open File to browse all saved letters in SQLite. Selecting a letter opens that specific workspace with its draft, version history, endorsement counts, and version-specific export links.
Creating a revision from a frozen version copies that frozen snapshot into a new editable draft for the same letter. The source frozen version remains immutable, endorsements stay attached to that source version, and prior exports remain tied to their original frozen version.
OpenLOS deletion is local-only. It removes records from this local SQLite-backed OpenLOS instance; it does not revoke, delete, edit, or retrieve files that were already exported, emailed, downloaded, printed, or shared elsewhere. In short, local-first deletion is not global revocation.
Local asset controls use explicit confirmation language:
- Delete local copy for a letter removes the local letter record, its active draft, frozen versions, signature requests, endorsements/signatures, and export records.
- Delete local draft removes only the active editable draft. Prior frozen versions and source frozen versions remain.
- Delete frozen version locally removes that frozen version plus requests, endorsements/signatures, and export records attached to that version. It does not delete sibling frozen versions or the parent letter.
- Remove endorsement from this local instance removes the local endorsement/signature record for that frozen version.
- Delete local request removes the request link record. Existing endorsements are kept unless the endorsement, frozen version, or letter is explicitly deleted.
- Delete export record removes only local export history. Previously exported or shared PDF/DOCX files are not affected.
Hosted and multi-user deletion semantics are future work. OpenLOS currently treats deletion as local archive management, not as legal revocation, signer-side self-service, or deletion from someone else's inbox, downloads folder, hosted app, tunnel session, or exported document.
- Write draft.
- Save draft to local SQLite.
- Freeze an immutable version.
- Create a supporter request.
- Supporter opens the request URL.
- Supporter reviews the frozen version.
- Supporter submits an endorsement back to the running OpenLOS app.
- Endorsement persists in local SQLite.
- Candidate exports the frozen artifact.
The supporter must be able to reach your running OpenLOS instance. If they cannot open the request URL, they cannot submit an endorsement.
OpenLOS includes a local settings page at /settings/connectivity for SMTP, APP_BASE_URL, and optional tunnel workflow guidance. Environment variables override matching settings saved through the UI. UI-saved connectivity settings are stored in local SQLite; SMTP passwords are plaintext in this MVP, so use environment variables or protect the local database when stronger secret handling is required.
APP_BASE_URL controls the links sent or copied for supporters.
Examples:
APP_BASE_URL="http://localhost:3000"
APP_BASE_URL="http://192.168.1.25:3000"
APP_BASE_URL="https://your-subdomain.trycloudflare.com"Use the right value for the sharing context:
http://localhost:3000works only on the same machine.http://192.168.x.x:3000may work on the same LAN.- A tunnel or domain URL lets external supporters reach your local OpenLOS instance.
Supporter submissions post back into the running OpenLOS app and persist in local SQLite.
Email sending requires SMTP configuration. OpenLOS will not create accounts, generate credentials, or configure external email providers for you.
Recommended SMTP providers:
- Gmail app passwords
- Fastmail
- Postmark
- Resend
- SMTP2GO
Without SMTP configured, OpenLOS still works: generate the request link and send it manually.
Example .env:
DATABASE_URL="file:./dev.db"
APP_BASE_URL="http://localhost:3000"
SMTP_HOST="smtp.example.com"
SMTP_PORT="587"
SMTP_USER="your-user"
SMTP_PASS="your-password-or-app-password"
SMTP_FROM_ADDRESS="you@example.com"
SMTP_FROM_NAME="OpenLOS"
SMTP_SECURITY="starttls"The legacy SMTP_FROM="OpenLOS <you@example.com>" format is still accepted and overrides SMTP_FROM_ADDRESS and SMTP_FROM_NAME when set.
If SMTP_HOST is blank, the app shows SMTP as unavailable and switches request creation to manual link generation. Use /settings/connectivity to save SMTP settings locally and send a test email. Test messages go only through the configured SMTP server; OpenLOS does not send secrets anywhere else.
Cloudflare Tunnel is a practical optional workflow for local-first external sharing. It is not required, and OpenLOS does not create or run tunnels automatically.
Run OpenLOS locally, then in a separate terminal run:
cloudflared tunnel --url http://localhost:3000Cloudflare prints a temporary public URL. Paste it into /settings/connectivity, or set it as APP_BASE_URL:
APP_BASE_URL="https://your-subdomain.trycloudflare.com"If using environment variables, restart OpenLOS so new request links use the tunnel URL. If using /settings/connectivity, save the value before creating new supporter requests.
What this means:
- OpenLOS still runs locally.
- SQLite remains local.
- The tunnel temporarily exposes supporter request links.
- Supporter submissions persist back into your local SQLite database.
- You can stop the tunnel when the request window is done.
OpenLOS separates request expiration from endorsement validity.
Request link expiration:
- request links automatically expire after 3 months
- candidates do not choose arbitrary request expiration dates
- expiration controls how long the request URL remains valid
Endorsement validity:
- defaults to 3 months from signing
- supporter may choose a shorter or longer duration
- minimum is 14 days
- maximum is 1 year
- invalid dates are rejected inline
Expired endorsements remain visible as expired instead of disappearing.
Exports are tied to a frozen version, not the editable draft.
The export target is a formal Letter of Support artifact, not a web-app screenshot. It includes:
- page 1 formal Letter of Support with candidate name, date, frozen version, and letter body
- page 2 and later endorsement table pages
- supporter full name, role/title, relationship, signed date, valid-through date, active or expired status, and signature
- typed, drawn, and uploaded signature images when available
- frozen timestamp
- content hash
- source URL provenance
- OpenLOS, Apache-2.0, copyright, generated timestamp, and page number footer metadata
DOCX download generates a real .docx using the existing docx dependency.
Direct PDF download is not enabled in this build. The app provides Open printable PDF view, which can be saved as PDF from the browser and is the Docker-safe supported path.
If direct PDF generation is added with Playwright later, local development requires installing a browser:
npx playwright install chromiumThe current Docker image does not bundle Chromium or Playwright system dependencies, so Docker keeps the printable view as the supported PDF path. DOCX export is independent and remains available.
OpenLOS focuses on provenance and human endorsement.
- Frozen versions are immutable snapshots.
- Endorsements attach to one frozen version.
- Content hashes make changes visible.
- Supporters control endorsement validity within the allowed bounds.
- Provenance metadata is visible in exports.
- OpenLOS does not claim legal e-signature compliance.
Copy the environment template:
cp .env.example .envInstall dependencies:
npm installGenerate Prisma client and create the local SQLite schema. The touch command creates the SQLite file that file:./dev.db resolves to from the Prisma schema directory:
npm run prisma:generate
touch prisma/dev.db
npx prisma migrate devStart the development server:
npm run devOpen:
http://localhost:3000
Useful validation commands:
npm run typecheck
npm run lint
npm run test:lineage
npx prisma validate
npm run buildBuild the image:
docker build -t openlos:local .Run with Docker Compose:
cp .env.example .env
docker compose up --buildThe app is exposed on port 3000 by default:
http://localhost:3000
If port 3000 is occupied:
OPENLOS_HOST_PORT=3001 docker compose up --buildThen open:
http://localhost:3001
Docker Compose persists SQLite data in the named volume openlos-sqlite, mounted at /data in the container. Compose intentionally sets the container database URL to the Docker path even if your local .env uses file:./dev.db:
DATABASE_URL="file:/data/openlos.db"The container creates /data/openlos.db if needed, then runs Prisma migrations before startup so a new volume gets the required tables.
Prisma resolves relative SQLite paths from the Prisma schema directory. For local development, DATABASE_URL="file:./dev.db" resolves to prisma/dev.db. Avoid DATABASE_URL="file:./prisma/dev.db"; from the schema directory that can resolve to prisma/prisma/dev.db.
DATABASE_URL: SQLite database path. Local npm development should usefile:./dev.db; Docker usesfile:/data/openlos.db.APP_BASE_URL: base URL used in supporter request links.OPENLOS_HOST_PORT: host port used by Docker Compose.SMTP_HOST: SMTP server host. Blank means manual links only.SMTP_PORT: SMTP server port, usually587or465.SMTP_USER: optional SMTP username.SMTP_PASS: optional SMTP password or app password.SMTP_FROM_ADDRESS: email sender address.SMTP_FROM_NAME: optional email sender display name.SMTP_SECURITY:starttls,ssl, ornone.SMTP_FROM: backward-compatible full sender header; overrides split from fields when set.
Development prerequisites:
- Node.js 20.11 or newer
- npm
- Docker, if validating the container path
Common workflow:
cp .env.example .env
npm install
npm run prisma:generate
touch prisma/dev.db
npx prisma migrate dev
npm run devPrisma commands:
npm run prisma:generate
npx prisma migrate dev
npm run prisma:deploy
npx prisma validateDocker commands:
docker build -t openlos:local .
docker compose config
docker compose up --buildMockup references live in:
docs/mockups/
OpenLOS intentionally avoids:
- LinkedIn-style profiles
- social feeds
- followers
- endorsement scores
- engagement metrics
- recruiter analytics
- enterprise HR workflows
- legal e-signature compliance claims
- AI-generated endorsements
- blockchain, tokens, or web3 proofs
- Go code
Apache-2.0.
Copyright 2026 Gregory Kulp