Pullboard
A work board that lives in your git repo. Agents pick up the next task, another agent checks it, and you barely explain a thing. Local, open source, no account.
A layered, real-time TypeScript framework for web APIs — services as contracts, repositories for data access, one hook pipeline for every transport and every agent.
Developer toolsTypeScript · JavaScriptGitHub @MantleJsOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Full-stack TypeScript framework for real-time apps and agent-native APIs: Clean Architecture services, pluggable database adapters, and MCP built in.
Taken from the repository README
Loading the full README.
Read the full README ↗A full-stack, tech-agnostic JavaScript/TypeScript framework optimized for building real-time applications and scalable web APIs. Mantle is the application layer between your UI and your data — analogous to the geological mantle beneath the crust. Your domain sits at the core and defines the interfaces it needs; database adapters implement them from the outside.
Mantle is designed to be architecture-first: it enforces a clean separation of concerns inspired by Clean Architecture and Onion Architecture, rather than coupling business logic to data access or transport concerns.
Mantle is built for three audiences: indie developers who want structure without ceremony, startup teams who need conventions that scale, and AI coding agents (Claude, Gemini, GPT-4o) that scaffold or generate backend code on behalf of a developer. Consistent naming, explicit interfaces, and clear layer boundaries make Mantle code correct to generate — an AI agent can produce a valid, runnable service with no human correction.
FeathersJS is the closest analog to Mantle. It is a lightweight, real-time capable service framework with a strong following. However, it carries a structural limitation: services couple business logic directly with data access logic. A Feathers service is simultaneously a use case, a repository, and a controller. This works well for small applications but becomes a liability at scale, during testing, or when swapping infrastructure.
Other frameworks (NestJS, AdonisJS, Hapi) either impose too much framework opinion on domain code, require significant boilerplate, or are not optimized for real-time delivery.
Mantle takes direct inspiration from FeathersJS's ergonomics — the plugin model, adapters, hooks-style middleware — but reimagines the core around a layered architecture where:
| Concern | FeathersJS | Mantle JS |
|---|---|---|
| Service definition | Class implementing data + logic | Interface (contract) only |
| Data access | Inside the service via adapter | Repository in Infrastructure layer |
| Business logic | Mixed into service methods | Isolated in Application / Domain |
| Hooks | Transport-aware | Transport-agnostic |
| Swapping databases | Requires service changes | Swap adapter only |
| Testability | Requires mocking transport | Domain testable with no mocks |
| AI scaffoldability | Moderate | High (clear boundaries) |
Transport Layer Express adapter — routes HTTP to services
Application Layer Service<T> contracts, hook pipeline (a.k.a. THE MANTLE)
Domain Layer (Core) Entities, Repository<T> interfaces
Infrastructure KnexRepository — implements Domain interfaces
Dependencies always point inward. Nothing in Domain or Application layers knows about HTTP or databases.
| Package | Description |
|---|---|
@mantlejs/mantle | Framework kernel — Service, Repository, hooks, errors. Zero external deps. |
@mantlejs/express | Express HTTP transport adapter |
@mantlejs/koa | Koa HTTP transport adapter |
@mantlejs/http | Zero-dependency HTTP transport adapter (Node.js handler + Fetch API / Edge Functions) |
@mantlejs/knex | SQL adapter via Knex.js (PostgreSQL, MySQL/MariaDB, SQLite3, MSSQL) |
@mantlejs/dynamodb | Amazon DynamoDB adapter — DynamoDbRepository with full QueryParams support |
@mantlejs/supabase | Supabase adapter — SupabaseRepository backed by the Supabase JS client |
@mantlejs/pinecone | Pinecone vector database adapter — PineconeRepository with embedding support |
@mantlejs/qdrant | Qdrant vector database adapter — QdrantRepository with embedding support |
@mantlejs/neo4j | Neo4j graph database adapter — Neo4jRepository with full QueryParams support |
@mantlejs/mongodb | MongoDB adapter — MongoRepository + Atlas Vector Search via MongoVectorRepository |
@mantlejs/auth | JWT engine + strategy runner |
@mantlejs/auth-local | Local email+password strategy (Argon2id) |
@mantlejs/auth-oauth | Shared OAuth 2.0 base — state, PKCE, find-or-create, route registration |
@mantlejs/auth-google | Google Sign-In strategy (authorization code + PKCE, no Passport.js) |
@mantlejs/auth-github | GitHub Sign-In strategy (authorization code flow, no Passport.js) |
@mantlejs/auth-facebook | Facebook Sign-In strategy (authorization code flow, no Passport.js) |
@mantlejs/auth-apple | Sign in with Apple strategy (form_post callback, ES256 client-secret JWT, no Passport.js) |
Experimental packages publish under the experimental npm dist-tag (npm install @mantlejs/mcp-code@experimental).
The Mantle UI registry — React Aria blocks installed with shadcn add @mantle/<block> — and the
mantlejs.com docs site aren't npm packages; they ship by deploy.
import { mantle } from "@mantlejs/mantle";
import { express } from "@mantlejs/express";
import { knex } from "@mantlejs/knex";
import { auth, authenticate, sanitizeUser } from "@mantlejs/auth";
import { localStrategy, hashPassword } from "@mantlejs/auth-local";
import { logger, createLogger, logRequest, logError } from "@mantlejs/logger";
const app = mantle()
.configure(express())
.configure(knex({ client: "pg", connection: process.env.DATABASE_URL }))
.configure(auth({ secret: process.env.JWT_SECRET! }))
.configure(localStrategy())
.configure(logger(await createLogger({ gcp: process.env.NODE_ENV === "production" })));
app.use("/users", new UserService(new UserRepository(app)), {
methods: ["find", "get", "create", "update", "patch", "remove"],
});
const requestLogger = logRequest();
app.service("users").hooks({
before: {
create: [hashPassword()],
all: [authenticate("jwt"), requestLogger],
},
after: { all: [sanitizeUser(), requestLogger] },
error: { all: [requestLogger, logError()] },
});
app.listen(3030);
For the smallest possible runnable app — one file, @mantlejs/http + @mantlejs/memory, no
external infra — see examples/todo-minimal. Two other runnable
examples live under examples/: realtime-chat (Socket.IO + local auth) and
knowledge-base (the canonical example — nearly every package wired into one app).
RepositoryService<T> is the framework-owned bridge from HTTP query strings to a single
Repository<T> — reach for it whenever a service reads and writes exactly one collection.
When a service legitimately needs more than one (writing an activity-log entry alongside the
record it just created, upserting an embedding after a text write, …), compose the repositories
directly behind a hand-written Service<T> instead of trying to force a second repository through
RepositoryService:
import type { Id, Repository, Service, ServiceParams } from "@mantlejs/mantle";
class ArticleService implements Service<Article> {
constructor(
private readonly articles: Repository<Article>,
private readonly activity: Repository<ActivityEntry>,
) {}
async find(params?: ServiceParams) {
return this.articles.findAll({ where: params?.query });
}
async get(id: Id) {
return this.articles.findById(id);
}
async create(data: Partial<Article>) {
const article = await this.articles.save(data);
await this.activity.save({ articleId: article.id, action: "created" });
return article;
}
// ...update/patch/remove delegate to `this.articles`
}
app.use("/articles", new ArticleService(new ArticleRepository(app), new ActivityRepository(app)));
Registered this way, a multi-repository service runs through app.use() exactly like a
RepositoryService — the full hook pipeline (before/after/error) and service:event
emission apply identically; hooks and transports cannot tell the difference.
Cross-repository writes are not atomic. If the second write fails, the first has already
committed and is not rolled back — the same non-atomicity as app.batch() (see the Phase 4
batch-atomicity notes). Whether that matters depends on the repositories involved:
KnexRepository-derived instance can share a
transaction — pass a withTransaction-scoped repository into the service (or construct the
service inside the transaction callback) so both writes commit or roll back together.Formalized once, so every cross-adapter secondary write follows the same shape instead of each service inventing its own: an idempotent upsert keyed on the source record's id, safe to retry, and non-fatal on failure (logged, never rolled back, never rethrown into the caller). Concretely:
@mantlejs/embeddings's embed() hook is the reference
implementation: it upserts a vector into a VectorRepository<T> keyed on the just-written record's
id (property 1 — and idempotency here comes for free from upsertVector's own upsert contract, not
from anything the hook does), and catches an embedding-provider or vector-store failure internally
rather than rethrowing it (property 3). Reach for the same shape for any other cross-adapter
secondary write (an activity log in a different store, a search-index sync, a cache invalidation) —
it doesn't need to be embeddings-specific.
This monorepo is managed with Nx.
# Build
npx nx build mantle # build one package
npx nx run-many -t build # build all packages
# Test
npx nx test mantle # test one package
npx nx run-many -t test # test all packages
# Lint
npx nx run-many -t lint
# Visualise the dependency graph
npx nx graph
NX_DAEMON=false npx nx g @nx/js:library \
--name=<name> \
--directory=packages/<name> \
--bundler=tsc \
--unitTestRunner=vitest \
--linter=eslint \
--publishable \
--importPath=@mantlejs/<name>
| Decision | Choice | Reason |
|---|---|---|
| Monorepo | Nx (TS preset, npm) | Task pipeline, module boundary enforcement |
| SQL adapter (current) | @mantlejs/knex via Knex.js | Query builder not ORM — keeps infra layer thin; additional adapters (e.g. Prisma) can be added |
| Password hashing | @node-rs/argon2 (Argon2id) | OWASP recommended; no 72-char bcrypt limit |
| Testing | Vitest | Faster than Jest, native ESM, Jest-compatible API |
| HTTP transport (P1) | @mantlejs/express via Express | Phase 1 adapter; transport layer is pluggable |
| Bundler | tsc | Emits .d.ts natively — critical for TS-first libraries |
@mantlejs/auth-microsoft | Microsoft Sign-In strategy (Entra ID, authorization code + PKCE, no Passport.js) |
@mantlejs/auth-linkedin | LinkedIn Sign-In strategy (OpenID Connect, authorization code flow, no Passport.js) |
@mantlejs/auth-twitter | Sign in with X (Twitter) strategy (authorization code + PKCE, no Passport.js) |
@mantlejs/auth-redis | Redis-backed RefreshTokenStore + OAuth StateStore for multi-instance deployments |
@mantlejs/storage | File upload via busboy, local disk storage |
@mantlejs/storage-s3 | AWS S3 storage adapter for @mantlejs/storage (supports MinIO, R2, and other S3-compatible stores) |
@mantlejs/storage-gcs | Google Cloud Storage adapter for @mantlejs/storage |
@mantlejs/cli | Command-line interface — scaffold projects, services, repositories, and hooks |
@mantlejs/logger | Structured logging — pino adapter, logRequest / logError hooks, request ID |
@mantlejs/schema | TypeBox schema validation (validate) and field resolution (resolver) hooks |
@mantlejs/memory | In-memory Repository<T> implementation for testing and prototyping |
@mantlejs/config | Environment-aware configuration loading with optional schema validation |
@mantlejs/sync | Cross-instance event sync — broadcasts service:event emissions across processes via Redis (DragonflyDB-compatible) or Supabase Realtime |
@mantlejs/socketio | Socket.IO transport adapter — real-time WebSocket events via the Mantle hook pipeline |
@mantlejs/openapi | OpenAPI 3.1 document generation from registered services, with optional Swagger UI |
@mantlejs/mcp | MCP server — expose services as Model Context Protocol tools (stdio + HTTP), full hook pipeline, deny-by-default |
@mantlejs/mcp-code | MCP code mode — typed API + QuickJS sandbox so agents run one script instead of many tool calls (experimental) |
@mantlejs/audit | Audit-trail hook — who did what, as which identity (user or agent), written to any Repository<T> sink |
@mantlejs/ratelimit | Rate limiting — rateLimit() hook keyed by agent, user, tenant, or IP; memory + Redis stores; IETF RateLimit headers |
@mantlejs/idempotency | Idempotency-Key support — safe create/patch retries over any Repository<T>, with replay, 409, and 422 |
@mantlejs/otel | OpenTelemetry tracing — a span per service call, parented on the incoming W3C traceparent, with hooks as span events |
@mantlejs/embeddings | Auto-embed-on-write hook — pluggable embedding provider, upserts into a VectorRepository<T> |
@mantlejs/client | Official client SDK — REST + Socket.IO, auth with token rotation, from browsers/Node.js/React Native |
@mantlejs/react | React bindings — hooks and providers over the Mantle client SDK |
create-mantlejs | Project initializer — scaffold a new Mantle application via npm create mantlejs |