A self-hosted, governed data-access platform: connect a SQL database, and it introspects the schema and instantly serves a fully documented OData v4 API, a REST/JSON API, and a built-in MCP server for AI clients — all behind one RBAC + audit pipeline.
Inspired by DreamFactory, built on
.NET 10 with an engine that also targets netstandard2.0 so it can be embedded in existing
.NET Framework 4.8 sites (see spec/15-embedded-library.md).
- Zero-code APIs: connect PostgreSQL, MySQL/MariaDB, SQL Server, or SQLite; get CRUD,
$filter/$select/$orderby/$top/$skip/$count,$expand,$apply,$batch, and OpenAPI 3.1 + CSDL docs — with no restarts. - Governed access: role-based permissions down to table, verb, row (filters), and field (deny/mask/write-only). The same policy engine secures OData, REST, and MCP.
- AI-ready: an MCP endpoint exposes per-identity, role-scoped tools so LLMs query data through structured, audited calls instead of raw SQL.
- Operable: API keys + JWT, rate limiting, buffered audit log, Prometheus metrics, health checks, single container image.
cd deploy/compose
docker compose up -d # full profile: platform + Postgres + Redis + sample DB
# or the single-container profile:
docker compose -f docker-compose.minimal.yml up -dOpen http://localhost:8080 and complete the first-run setup wizard.
Everything below is also doable in the admin console at /; here it is as raw HTTP so
the flow is explicit. Assume the server is at http://localhost:8080.
# First run only — creates the system administrator:
curl -X POST http://localhost:8080/system/setup \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","displayName":"Admin","password":"a-strong-password-1"}'
# Log in to get a JWT:
TOKEN=$(curl -s -X POST http://localhost:8080/system/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"a-strong-password-1"}' | jq -r .accessToken)curl -X POST http://localhost:8080/system/services \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"name": "sales",
"label": "Sales DB",
"connectorType": "postgresql",
"connection": { "host": "db.internal", "port": 5432, "database": "sales",
"username": "api_reader", "password": "secret",
"tls": { "mode": "prefer" } }
}'The schema is introspected in the background; the service flips to Active and the APIs
go live with no restart. (connectorType is one of postgresql, mysql, sqlserver,
sqlite; SQLite uses "connection": { "filePath": "/data/app.db" }.)
# A read-only role, US rows only, ssn hidden:
ROLE=$(curl -s -X POST http://localhost:8080/system/roles \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"analyst","isActive":true,"access":[
{"serviceName":"sales","resourcePattern":"*","verbs":["GET"],"effect":"allow",
"rowFilter":"country eq '\''US'\''",
"fieldPolicies":[{"fieldPattern":"ssn","action":"deny"}]}]}' | jq -r .id)
APP=$(curl -s -X POST http://localhost:8080/system/apps \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"name\":\"analyst-app\",\"roleId\":$ROLE,\"isActive\":true,\"mcpEnabled\":true}" | jq -r .id)
# The full key is returned exactly once:
KEY=$(curl -s -X POST http://localhost:8080/system/apps/$APP/keys \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"default"}' | jq -r .key)# OData v4 (clients: Excel, Power BI, Microsoft.OData.Client, etc.)
curl "http://localhost:8080/api/odata/sales/customers?\$filter=country eq 'US'&\$top=10&\$expand=orders" \
-H "X-API-Key: $KEY"
curl "http://localhost:8080/api/odata/sales/\$metadata" -H "X-API-Key: $KEY" # CSDL
curl "http://localhost:8080/api/odata/sales/openapi.json" -H "X-API-Key: $KEY" # OpenAPI 3.1
# REST/JSON dialect (simpler, SQL-ish filter)
curl "http://localhost:8080/api/rest/sales/_table/customers?filter=country='US'&limit=10" \
-H "X-API-Key: $KEY"
# Write (subject to the same RBAC)
curl -X POST "http://localhost:8080/api/odata/sales/customers" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"name":"Acme","email":"ops@acme.example","country":"US"}'The key only sees what its role allows: ungranted tables 404, denied fields never appear, row filters are always applied — identically across OData, REST, and MCP.
Point Claude Desktop / Cursor at the MCP endpoint with the same key:
{ "mcpServers": { "ez-odata": {
"url": "http://localhost:8080/mcp",
"headers": { "X-API-Key": "<your key>" } } } }The tool catalog is generated per-key, so the model only sees sales_query,
sales_list_tables, etc. for tables that key can actually read.
Add the OData/REST engine to an ASP.NET Core app you already run — pointed at a database,
with services and roles declared in code and no system database. See
src/EzOdata.AspNetCore/Embedded/README.md:
builder.Services.AddEzOData(ez =>
{
ez.AddService("sales", s => s.UsePostgreSql(connection).Options(o => o.ReadOnly()));
ez.AddRole("readonly", r => r.Allow("sales", "*", Verb.Get, rowFilter: "owner_id eq @identity.sub"));
ez.ResolveRolesBy(user => user.IsInRole("Admin") ? ["readonly"] : []);
});
app.MapEzOData("/api/odata");The engine targets netstandard2.0, so the same packages run on .NET Framework 4.8
(classic ASP.NET Web API 2 via EzOdata.WebApi) as well as modern .NET.
Host-auth pass-through helpers
The three ResolveRolesBy variants let you skip writing a manual lambda:
// Your ASP.NET roles map directly to ez roles of the same name:
ez.UseHostRoles();
// Roles come from a custom claim (e.g. a JWT scope claim):
ez.MapRolesFromClaim("scope", raw => raw.Replace("data:", ""));
// Full escape hatch — any ClaimsPrincipal -> role name logic:
ez.ResolveRolesBy(user => user.HasClaim("dept","ops") ? ["ops"] : []);sub/email and all other claims are automatically forwarded so @identity.*
row-filter expressions work without extra configuration.
No-auth dev mode
For local development without standing up a full identity provider, add one flag. This is hard-blocked in any environment other than Development — the server refuses to start if the flag is set in staging or production.
Standalone server — set in appsettings.Development.json (or as an env var):
{ "Auth": { "DevNoAuth": true } }# Railway dev environment variables:
ASPNETCORE_ENVIRONMENT=Development
Auth__DevNoAuth=trueEmbedded adapter:
builder.Services.AddEzOData(ez =>
{
ez.AddService("sales", s => s.UsePostgreSql(connection));
ez.AllowAnonymousInDevelopment(); // no-op outside Development, throws in Production
});In dev no-auth mode every request carries a full-access bypass identity — trimming is disabled and the policy engine is short-circuited. A warning banner is logged on startup.
The embeddable packages (EzOdata.AspNetCore, EzOdata.WebApi, EzOdata.Core, the
connectors, etc.) are published to GitHub Packages by CI. To consume them, add the
source:
dotnet nuget add source "https://nuget.pkg.github.com/YOLOVibeCode/index.json" \
--name ezodata --username <you> --password <github-token-with-read:packages>
dotnet add package EzOdata.AspNetCore# Backend (set a dev master key + signing key; see appsettings.Development.json)
dotnet run --project src/EzOdata.Host
# Admin UI (dev server with API proxy)
cd ui && npm install && npm run devThe engine (Core, connectors, OData, Rest, Docs) is netstandard2.0; the standalone
server, EF system store, MCP, and admin API are modern .NET. All three protocols compile to a
shared Query IR that flows through one policy engine and one set of connectors — there is
exactly one authorize-and-execute path. See spec/ for the full specification.
src/
EzOdata.Core # schema model, Query IR, policy engine, security contracts
EzOdata.Connectors.* # PostgreSQL / MySQL / SqlServer / Sqlite + shared SQL compiler
EzOdata.OData / .Rest # protocol engines (ODL-direct + REST dialect)
EzOdata.Mcp # MCP JSON-RPC server
EzOdata.Docs # CSDL + OpenAPI 3.1 generation
EzOdata.Data # EF Core system store, background workers
EzOdata.Admin # /system management API
EzOdata.AspNetCore # ASP.NET Core host adapter (MapEzOData / MapEzODataRest)
EzOdata.Host # standalone server (composition root)
EzOdata.Cli # ez-admin operations CLI
ui/ # React 19 admin console
deploy/ # Dockerfile, compose, Helm
spec/ # product & engineering specification (16 docs)
dotnet test # unit + integration (Testcontainers) + OData conformance + security suite
cd ui && npx playwright test # admin console smoke (requires a running host)| Doc | What it covers |
|---|---|
| Why OData for data grids | Concrete $filter/$select/$orderby/$count/$expand examples; comparison with hand-rolled endpoints; Excel/Power BI connection |
| spec/ | Full product & engineering specification (16 docs) |
| Embedded adapter README | Bolt-on NuGet usage, host-auth helpers, dev no-auth |
Apache-2.0 — Copyright 2026 Noctusoft, Inc.