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
@launchstack/core is not published to npm — the registry returns 404, so pnpm add @launchstack/core will not resolve. The package itself is publish-ready (publishConfig redirects main, types and the whole exports map to ./dist, and the release workflow runs publint and attw against the packed tarball). Two mechanical things block an actual release:
.changeset/does not exist, so Changesets cannot version or publish.release.yml:20is gated ongithub.repository == 'launchstack/launchstack', but this repository isDeodat-Lawson/LaunchStack, so the release job is skipped on every push.
To try Launchstack today, run the reference 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, CLERK_SECRET_KEY, and one OpenAI-compatible chat endpoint — set CHAT_BASE_URL (plus CHAT_API_KEY when that endpoint needs a credential). There is no built-in default endpoint and no per-vendor variable: a bare OPENAI_API_KEY, OPENROUTER_API_KEY or OLLAMA_BASE_URL will not configure chat. 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 — db:push runs ensure-pgvector.mjs, which exits non-zero on stock Postgres.
pnpm --filter @launchstack/web db:push # sync Drizzle schema
pnpm --filter @launchstack/web dev # Next.js + Inngest on :3000 and :8288If you ran
make upfirst, note that Compose publishes Postgres on host port 5433 with databasepdr_ai_v2, while.env.exampleshipslocalhost:5432/pdr_ai. PointDATABASE_URLatlocalhost:5433/pdr_ai_v2to reuse the container's database.
Windows (no make)
docker compose --env-file .env up --build -d # lite
docker compose --env-file .env --profile ocr -f docker-compose.yml -f docker-compose.ocr.yml 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.
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 reference app. Its Dockerfiles live beside it, not at the root. |
packages/core |
The engine. |
packages/features |
Vertical features built on core. |
services/, sidecar/ |
Standalone containers — two Python, one Node. Not part of the pnpm workspace; each manages its own dependencies. |
scripts/ |
ops/ for operational tasks, dev/ for manual developer probes. |
Two boundary caveats worth knowing up front, both tracked in REPOSITORY.md: packages/core currently contains the SaaS database schema, and the RAG pipeline implementation still lives under apps/web. Directory names are not yet a reliable guide to what is engine and what is product.
| 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 is meant to arrive 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, but the CI lint step iscontinue-on-error: trueagainst a legacy baseline, so violations do not block merges today — and six files in core currently violate the no-process.envrule.
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, Marker, 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 — OpenAI, OpenRouter, MiniMax, Together, Groq, 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://openrouter.ai/api/v1
CHAT_API_KEY=sk-or-v1-...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: openai/gpt-4o-mini
preset: openai/gpt-4o-mini
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.
The goal is for @launchstack/core to be a plain TypeScript library you can drop into any Node 20+ project with a Postgres database. Beyond the release plumbing in Status, two things still leak:
- It is not environment-independent. Six files under
packages/core/srcreadprocess.env:crypto/secret-box.tsandembeddings/company-config.tsdocument theirs as transitional fallbacks, butproviders/ner/llm.ts,providers/ner/sidecar.ts,providers/reranking/jina.tsandproviders/reranking/sidecar.tsdo not. The twosidecar.tsfiles readSIDECAR_URLat module load with a hardcodedhttp://localhost:8000default that cannot be set throughCoreConfig. - It still owns product concerns. The SaaS database schema lives in core, and
RagPort.companyEnsembleSearchis keyed on a requiredcompanyId: number, so consumers would inherit our tenancy model.
See REPOSITORY.md for the tracked list of boundary issues.
packages/core/README.mdis out of date — it still advertises anengine.ragAPI that does not exist, claims core reads zero environment variables, and tells you topnpm addan unpublished package. Treatapps/web/src/server/engine.tsas the reference until it is rewritten.
- 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
packages/core/should come with a Changeset — note thatpnpm changesetcurrently fails because.changeset/has not been initialised - ESLint declares the core/features/host import boundaries; don't work around them, even though CI does not yet enforce them
Licensed under the Apache License 2.0. By contributing you agree your contributions will be released under the same license.