A TypeScript engine for AI-native applications. Ingestion, OCR, RAG, knowledge graph, LLM abstractions, and background jobs — ports-based, and wired into a Next.js reference app that shows how the pieces fit together.
Run it · Repository layout · Packages · Architecture · Chat models · Contributing
The engine packages (@launchstack/protocol, evidence, application, adapters, and the core facade) are not yet on npm — the first release will publish them together through the Changesets flow in release.yml (the old hardcoded-repo gate and missing .changeset/ that blocked releases are fixed; the workflow validates the packed tarball with publint and a Node-ESM loadability check for every subpath). Until that first release lands, consume the engine by running this repository.
To try Launchstack today, run the app below.
Requirements: Node ≥ 20 and pnpm 10.15.1 (corepack enable picks up the pinned version).
git clone https://github.com/Deodat-Lawson/LaunchStack.git
cd LaunchStack
pnpm install
cp .env.example .envapps/web/src/env.ts will refuse to boot without DATABASE_URL and CLERK_SECRET_KEY. Chat needs no variable at all: with CHAT_BASE_URL unset it defaults to Google Gemini's OpenAI-compatible endpoint, authenticated with GOOGLE_AI_API_KEY. Set CHAT_BASE_URL (plus CHAT_API_KEY) to reach anything else. There is still no per-vendor variable: a bare OPENAI_API_KEY, OPENROUTER_API_KEY or OLLAMA_BASE_URL will not configure chat, and none of them is forwarded to the Gemini default — a key names who you are, not where the request goes, and every one of those providers speaks the same OpenAI chat-completions protocol, so each is reached through CHAT_BASE_URL like any other. Only AI_BASE_URL/AI_API_KEY, a straight rename of the canonical pair, is still translated for a release with a deprecation warning. See Chat models.
make up-prod # lite stack, detached (~400MB RAM)
make up-ocr # adds Docling for Office docs, detached (~1.2GB RAM)
make logs # follow logs
make down # stop containers (keeps volumes — DB + S3 data persists)
make down-clean # stop + wipe volumes (fresh DB on next up)make up also exists but runs in the foreground — you will need a second shell to run make down. Use make up-prod unless you want to watch the build. make up-fast builds Next.js on the host first and is the quickest iteration loop.
You need a Postgres with the pgvector extension available — the migration runner enables it and exits non-zero on stock Postgres.
pnpm --filter @launchstack/web db:migrate # apply BOTH migration sets (engine, then product)
pnpm --filter @launchstack/core db:seed # optional: one company/user/document
pnpm --filter @launchstack/web dev # Next.js on :3000
pnpm --filter @launchstack/worker dev # the durable worker on :8020 — ingestion runs here, not in web
pnpm --filter @launchstack/web inngest:dev # optional: Inngest dev UI on :8288, pointed at the worker's :8020/api/inngestweb dev is plain next dev — it accepts uploads but processes nothing.
Run the worker alongside it or documents will sit queued forever; the
Inngest dev server is only needed for the Inngest-hosted background
verticals (trend search, prospector, …), not for ingestion.
db:migrate is the same command CI, Docker and the Vercel production build run.
Running it from @launchstack/core applies only the engine set — that is what a
consumer embedding the engine uses, not what a full app needs.
Nothing else creates schema — see Changing the database.
.env.examplepointsDATABASE_URLatlocalhost:5433/pdr_ai_v2, which is what Compose publishes — so the Docker and non-Docker paths share one database by default. Running your own Postgres instead? Use the commentedlocalhost:5432/pdr_ailine in its place.
Windows (no make)
docker compose --env-file .env up --build -d # lite
docker compose --env-file .env --profile ocr up --build -d # with Docling
docker compose --env-file .env down --remove-orphans # stop (keeps volumes)
docker compose --env-file .env down -v --remove-orphans # stop + wipe volumesOr install make via Chocolatey (choco install make) or Scoop (scoop install make).
The repository root is not an application. It is a pnpm workspace: no runtime dependencies, no server, no app code.
pnpm devat the root fails withERR_PNPM_NO_SCRIPT— always target a package with--filter. Only repo-wide commands live at the root:lint,lint:fix,typecheck,check,format:write,format:check, and the Changesets scripts.
See CONTRIBUTING.md for the full dev guide.
A deployment is self-hosted unless you set DEPLOYMENT_MODE=cloud, and the
defaults are chosen for that case: usage is recorded but never gated, no
telemetry is loaded, no assets are fetched from a CDN, and the instance
identifies itself by the host you serve it from. The first person to sign up
becomes the owner of the workspace they create, already verified — there is no
separate admin bootstrap.
This repository builds the application. The marketing site at launchstack.app is
a separate app (apps/landing) that Docker and Compose do not
build — on your own instance, / is the sign-in page, not a pitch.
Full details, including what degrades without which optional service, are in Self-hosting defaults.
REPOSITORY.md is the map: every directory, all deploy targets, and which areas are engine versus hosted product. Read it before the layout below surprises you.
| Path | What it is |
|---|---|
apps/web |
The Next.js app: UI, auth, command acceptance, synchronous reads. |
apps/landing |
The public site (launchstack.app): landing, pricing, contact, deployment guide. No database, no auth; not part of any self-hosted deployment. |
apps/worker |
The durable workflow coordinator — consumes the ingestion outbox and hosts the background jobs (ADR-003). |
packages/protocol, packages/evidence, packages/application, packages/adapters |
The layered engine (ADR-002): contracts → pure company-state logic → use cases/ports → implementations. |
packages/core |
The published compatibility facade over the engine packages. |
packages/features |
Vertical features built on the engine. |
services/ |
Compute services — document-converter (Node), transcription and document-editor (Python). Not part of the pnpm workspace; each manages its own dependencies. |
scripts/ |
ci/ for gates, ops/ for operational tasks, dev/ for manual developer probes. |
| Package | Status | What it does |
|---|---|---|
@launchstack/core |
unpublished | The engine: db, llm, embeddings, ocr, rag, graph, crypto, guardrails, ingestion, providers, storage, jobs, credits, errors. |
@launchstack/features/* |
internal | Vertical features built on core: adeu, client-prospector, company-metadata, doc-ingestion, legal-templates, marketing-pipeline, repo-explainer, trend-search, voice |
mcp, workflow-generation, rules-extraction, connectors |
roadmap | Scaffolding only — each is a README plus an index.ts containing export {}. Not declared in packages/features exports, so they are not importable yet. |
apps/web |
— | The Next.js reference app — how we wire everything together |
Features import core via subpath imports (@launchstack/core/db, @launchstack/core/ocr/processor, etc.). The reference app imports features and supplies the ports (storage, jobs, credits, RAG) that connect to real infrastructure.
Core exposes ports that the host wires up. Features depend only on these ports; they never reach into the app or the framework.
┌───────────── apps/web (Next.js host) ─────────────┐
│ env.ts → engine.ts → createEngine(config) │
│ │ │
│ └─ wires: StoragePort (S3) │
│ JobDispatcherPort (Inngest)│
│ CreditsPort (DB) │
│ RagPort (hybrid search) │
└──────────────────┬────────────────────────────────┘
│
┌──────────────────▼────────────────────┐
│ @launchstack/features/* │
│ (adeu, marketing-pipeline, ...) │
│ import via @launchstack/core/<sub> │
└──────────────────┬────────────────────┘
│
┌──────────────────▼────────────────────┐
│ @launchstack/core │
│ db · llm · embeddings · ocr · rag · │
│ graph · guardrails · ingestion │
└───────────────────────────────────────┘
- Core knows no framework. Config arrives through
CoreConfig. - Features can read
process.env, but cannot import from the host app. - Host owns env, auth, routing, and implements the ports.
eslint.config.jsdeclares these boundaries and CI enforces them: the lint step is blocking (ADR-006) and the tree lints clean. The engine packages read noprocess.env— the no-env rule is lint-enforced, not aspirational.
createEngine(config) opens the database pool and registers the storage, jobs, credits, RAG, database and Neo4j slots. Several subsystems are configured through separate registration calls — apps/web/src/server/engine.ts makes seven of them: configureAppChatModels, configureEmbeddingIndexRegistry, configureEmbeddingFactory, configureCompanyEmbeddingDefaults, configureProviders, configureSecretBox, and configureOcr. Chat is the odd one out: createEngine already applies config.llm.chat itself, so configureAppChatModels only re-registers the same configuration and your own host needs no separate chat call. Slots are read lazily, so what matters is that they are set before a subsystem is first used.
import { readFileSync } from "node:fs";
import { createEngine } from "@launchstack/core";
import { createChatModelsConfig } from "@launchstack/core/llm";
import { configureOcr } from "@launchstack/core/ocr/config";
import { configureSecretBox } from "@launchstack/core/crypto";
// …plus the embeddings and provider registrations listed above
const engine = createEngine({
db: { url: process.env.DATABASE_URL! },
llm: {
// Chat: one OpenAI-compatible endpoint; models and routes come from YAML.
chat: createChatModelsConfig({
yaml: readFileSync("apps/web/config/chat-models.yaml", "utf8"),
endpoint: {
baseUrl: process.env.CHAT_BASE_URL!,
apiKey: process.env.CHAT_API_KEY, // omit for keyless endpoints
},
}),
// Non-chat OpenAI-compatible work keeps its own credential — it must
// never borrow the chat endpoint's key.
openai: { apiKey: process.env.OPENAI_API_KEY! },
},
embeddings: { indexName: "legacy-openai-1536" },
ocr: { defaultProvider: "NATIVE_PDF" },
providers: {},
storage: myStoragePort, // you implement StoragePort (S3, local, …)
jobs: { dispatcher: myDispatcher }, // a JobDispatcherPort: { dispatch(), name }
});
const { db } = engine; // Drizzle client
await engine.close(); // graceful shutdownjobs.dispatcher is a port, not a vendor SDK — apps/web wraps its Inngest client in createAppJobDispatcherPort() rather than passing the client directly. Read engine.ts rather than this excerpt when wiring your own host.
createEngine and the registration functions both store state on globalThis (18 createSlot call sites across core). That is a deliberate defence against Next.js HMR re-evaluation and bundler dual-copies, but it means one engine per process.
apps/web is a Next.js app built on the engine. It demonstrates:
- Clerk employer/employee auth with role-aware middleware
- Document upload + optional OCR (
NATIVE_PDF, Docling, Azure, Landing.AI, Datalab) - PostgreSQL + pgvector semantic retrieval for RAG
- AI chat with agent guardrails (PII filter, grounding, confidence gate)
- Predictive document analysis — eight document types are defined, though the request validator currently accepts only
contract,financial,technical,complianceandgeneral - Marketing pipeline for Reddit, X, LinkedIn, Bluesky
- Inngest-backed background jobs
- Optional LangSmith tracing
Chat reaches one endpoint that implements the OpenAI chat-completions
protocol — Google Gemini (the default), OpenRouter, MiniMax, vLLM, llama.cpp, LM
Studio, Ollama's /v1 surface, and most gateways all qualify. Point
CHAT_BASE_URL at it and give it a credential if it needs one:
CHAT_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
CHAT_API_KEY=AIza...Gemini is the default: leave CHAT_BASE_URL unset and set GOOGLE_AI_API_KEY
instead, and chat reaches the same endpoint.
That endpoint can serve many models. Which model handles general chat,
cheap extraction, reasoning, and images is written in
apps/web/config/chat-models.yaml, where each model either references a
bundled preset or declares its own behavior:
version: 1
models:
primary:
id: gemini-2.5-flash
preset: google/gemini-2.5-flash
routes:
default: primaryBehavior is never inferred from a model id, and specialized routes fail closed: if no vision-capable model is configured, the image control is disabled rather than an image being sent to a model that will ignore it. See docs/chat-models.md for presets, route inheritance, the five reasoning modes, and how to add a preset.
Chat configuration is independent from embeddings, OCR, transcription, reranking, and text-to-speech; configure only the supporting capabilities you enable. Those never borrow the chat credential.
Docker Compose forwards CHAT_BASE_URL, CHAT_API_KEY, and
CHAT_MODELS_CONFIG from .env to the reference app container, and mounts
apps/web/config/chat-models.yaml so you can edit it without rebuilding.
The ingestion pipeline reads exports from common tools without requiring OAuth — just drop the files in:
| Source | Export | Adapter |
|---|---|---|
| Notion | Markdown & CSV / HTML | TextAdapter, HtmlAdapter |
| Google Docs / Sheets | DOCX / CSV / XLSX | DocxAdapter, SpreadsheetAdapter |
| Google Drive | Takeout ZIP | ZipAdapter (delegates per entry) |
| Slack | Workspace export JSON | JsonExportAdapter |
| GitHub | Code ZIP, gh issue/pr list --json |
ZipAdapter, JsonExportAdapter |
Plus first-class PDF, DOCX, PPTX, XLSX, MD, HTML, TXT, and image adapters.
ZIP caveat:
ZipAdapterskipsJsonExportAdapterandImageAdapterfor entries inside an archive. A Slack export shipped as a ZIP of JSON therefore yields no pages — unzip it and drop the loose.jsonfiles in instead.
@launchstack/core is the published compatibility facade over the layered engine packages (@launchstack/protocol / evidence / application / adapters — ADR-002): every historical subpath keeps working and re-exports from those packages, and CI proves each one loads under plain Node ESM (scripts/ci/check-package-exports.mjs). The engine packages read no process.env (lint-enforced) — configuration arrives through CoreConfig and typed ports; apps/web/src/server/engine.ts is the reference composition root.
One tenancy caveat remains by design: the engine's search contract is keyed on companyId, so a consumer adopts that workspace model (without inheriting auth, billing, or product tables). See REPOSITORY.md.
- Issues — github.com/Deodat-Lawson/LaunchStack/issues
- Security — email per SECURITY.md
We welcome PRs — start with CONTRIBUTING.md. A few things to know up front:
- One issue per PR
- Changes to the published engine packages should come with a Changeset (
pnpm changeset) - ESLint declares the core/features/host import boundaries and CI enforces them (lint is blocking); don't work around them
Licensed under the Apache License 2.0. By contributing you agree your contributions will be released under the same license.