neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
로그인과 접근 사이의 누락된 레이어, 모든 인증 제공자와 자체 DB에 조직·역할·권한·기능 플래그 제공
The missing layer between login and access. Organizations, roles, permissions and feature flags for any auth provider, on your own database.
개발 도구TypeScript · JavaScriptGitHub @CodexSploitx온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Universal Organization & Authorization Layer
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Organizations, teams, roles, permissions and feature flags — for any auth provider, on your own database.
Your auth provider answers "who is this user?". It shouldn't have to also answer "what organization are they in, and what can they do there?" — that's a different problem, and one you'll want to own regardless of which auth provider you pick, or move to next year.README 전문 보기 ↗Universal Organization & Authorization
Organizations, teams, roles, permissions and feature flags — for any auth provider, on your own database.
Español · Website · Documentation
Your auth provider answers "who is this user?". It shouldn't have to also answer "what organization are they in, and what can they do there?" — that's a different problem, and one you'll want to own regardless of which auth provider you pick, or move to next year.
UNIORA is the layer that sits between the two. It's not an auth provider — it plugs into the one you already use (Supabase, Clerk, Auth0, Better Auth) — and it's not a SaaS: the organizations, memberships, roles, permissions and audit logs it manages live in your own database, not ours.
Auth Provider UNIORA
│ │
"Who are you?" "What organization? What can you do?
What does your organization have enabled?"
│ │
└──────────────┬───────────────┘
▼
ALLOW / DENY
IdentityLinkRepository even lets a user migrate providers without losing their org, roles or permissions.uniora.* schema (uniora_* tables in SQLite) that won't collide with your app's own tables.AuthorizationEngine never grants access it can't confidently justify — unknown permission, missing membership, wrong organization: always DENY, never a silent fallback to ALLOW.MembershipRepository refuses to unassign the Owner role or delete a membership if it's the last one holding it — enforced with a real database constraint, not just application code.@uniora/core has zero dependency on Next.js, Express, or any specific database driver — adapters plug in around it.The same engine, rules and data model. Choose how it reaches your code:
| Embedded library | Standalone server | |
|---|---|---|
| Runs | Inside your backend process | As a separate service you operate |
| You call it with | uniora.can() | HTTPS and an API key (POST /v1/check), or the typed @uniora/client |
| Network hop | None | One request per decision |
| Languages | TypeScript and Node.js | Any language over REST (OpenAPI) |
| Data | Your database, in a dedicated uniora.* schema | Its own dedicated database |
| Access control | Your application code | API clients with scopes and organization lists |
| Start | npm install @uniora/core | npx uniora server start |
| Guide | Getting started | The server |
There is no UNIORA cloud. In both modes you run it and you own the data.
profiles.put over the API), and members can be found by name or e-mail (embedded, server).A local-first admin UI over your own database. Run it with npx uniora studio. It listens on 127.0.0.1 only, is unlocked by a random per-launch token and sends data nowhere. Every change it makes is audited, and --read-only makes it a safe viewer. Screenshots show a demo database.
import { createAuthorizationEngine, createMemoryStorage } from "@uniora/core";
// swap createMemoryStorage() for @uniora/postgres (or @uniora/sqlite) in production
const storage = createMemoryStorage();
const uniora = createAuthorizationEngine(storage);
const canDeleteVehicle = await uniora.can({
identity: { provider: "supabase", subject: userId },
organizationId: orgId,
permission: "vehicles.delete",
});
// Combined permission + feature-entitlement check
const canUseAI = await uniora.access.check({
identity: { provider: "supabase", subject: userId },
organizationId: orgId,
permission: "assistant.use",
feature: "ai_assistant",
});
Every organization is born with exactly one protected Owner — created atomically alongside the organization, never as an afterthought:
import { createOrganizationWithOwner } from "@uniora/core";
const { organization, membership } = await createOrganizationWithOwner(storage, {
organizationId: orgId,
organizationName: "Acme Motors",
// organizationSlug is optional — derived from the name when omitted
// ("Acme Motors" -> "acme-motors"), validated and enforced unique either way.
ownerRoleId: crypto.randomUUID(),
membershipId: crypto.randomUUID(),
ownerIdentity: { provider: "supabase", subject: userId },
});
// membership's owner now passes every uniora.can(...) check for this organization,
// regardless of permissionKeys — see @uniora/core's docs for why.
Resolving identity from a real session is one call to the matching adapter:
import { resolveIdentity } from "@uniora/supabase"; // or @uniora/clerk, @uniora/auth0, @uniora/better-auth
const identity = await resolveIdentity(supabaseClient, accessToken); // Identity | null
Identity adapters only ever answer "who is this?" — never "what can they do?". That decision always belongs to the AuthorizationEngine.
Server computes a bounded AuthorizationSnapshot, client UI just reads it — @uniora/react never talks to your database:
// server (e.g. a Server Component, an API route)
import { computeAuthorizationSnapshot } from "@uniora/core";
const snapshot = await computeAuthorizationSnapshot(engine, storage.features, {
identity,
organizationId,
permissions: ["vehicles.create"],
features: ["advanced_reports"],
});
// client
import { UnioraProvider, Can, Feature } from "@uniora/react";
<UnioraProvider snapshot={snapshot}>
<Can permission="vehicles.create">
<CreateVehicleButton />
</Can>
<Feature feature="advanced_reports">
<AdvancedReportsPanel />
</Feature>
</UnioraProvider>
<Can>/<Feature>/useCan/useFeature are UX only and deliberately headless (no design system baked in) — hiding a button is never a substitute for the server independently authorizing the real operation. See apps/playground for a full working example built on shadcn/ui + ReUI.
For Next.js specifically, @uniora/next wires the pieces above into the App Router's own patterns — no hard dependency on the next package itself, just react.cache() for request-scoped memoization and the standard Response/Request APIs:
// server — Server Action or Route Handler
import { assertCan, authorizeRoute } from "@uniora/next";
// throws AuthorizationDeniedError — for a Server Action
await assertCan(engine, { identity, organizationId, permission: "vehicles.delete" });
// returns a ready-to-return Response | null — for a Route Handler
const denied = await authorizeRoute(engine, { identity, organizationId, permission: "vehicles.delete" });
if (denied) return denied;
For Express, @uniora/express is a route middleware — again with no dependency on the express package itself. It answers 401 when you can't tell who the caller is and 403 when the engine denies, and any error (yours, the engine's, the database's) goes to next(err): a failure can never let a request through. It does not rely on Express 5's async-error handling, so it suits Express 4 too.
import { requirePermission, requireFeature } from "@uniora/express";
app.delete(
"/orgs/:orgId/vehicles/:id",
requirePermission(engine, "vehicles.delete", {
// identity: whatever your auth middleware resolved (via @uniora/supabase, @uniora/clerk, ...)
resolve: (req) => req.identity && { identity: req.identity, organizationId: req.params.orgId },
}),
deleteVehicle,
);
// requireFeature(engine, "ai_assistant", { resolve }) checks what the organization has unlocked;
// authorize(engine, { permission, feature, resolve }) checks both.
Full documentation lives in guides/:
Getting started · Concepts · Core reference · Storage · Identity adapters · Express, Next.js and React · Invitations · Audit log · CLI, Studio and configuration · Errors · Hardening.
| Package | What it does |
|---|---|
@uniora/core | Framework-independent domain model: Organizations (with a protected Owner role, created atomically), Custom Roles, Permissions, Features, Audit Logs, cross-provider Identity Linking, Invitations (single-use tokens, delivery engine with retries), and the deny-by-default AuthorizationEngine. |
@uniora/postgres | PostgreSQL storage adapter (dedicated uniora.* schema) + migrations. |
@uniora/sqlite | SQLite storage adapter over better-sqlite3 (uniora_* tables) + migrations. |
@uniora/mailer-smtp | SMTP sender for invitations: configured from UNIORA_SMTP_* in .env, with professional multilingual e-mail templates. |
@uniora/supabase | Identity adapter for Supabase Auth. |
@uniora/clerk | Identity adapter for Clerk. |
@uniora/better-auth | Identity adapter for Better Auth. |
UNIORA owns the invitation flow — a single-use token (only its SHA-256 is stored), role(s), expiry, resend/revoke, accept, rate limits and an audit trail — and hands delivery to a pluggable InvitationSender. Use @uniora/mailer-smtp for a ready-made SMTP sender with professional, multilingual (en/es/pt/fr/de/it), dark-mode-aware templates, or write your own sender for Resend, SES, a queue...
import { createInvitationService } from "@uniora/core";
import { createSmtpInvitationSenderFromEnv } from "@uniora/mailer-smtp";
const invitations = createInvitationService({
storage,
acceptUrl: (token) => `https://app.example.com/invite/${token}`,
sender: createSmtpInvitationSenderFromEnv(), // reads UNIORA_SMTP_* from .env, fails fast if incomplete
});
// Authorize first (e.g. engine.can(... "members.invite")) — the service does not.
const { acceptUrl, delivery } = await invitations.invite({
organizationId, email: "[email protected]", roleIds: [editorRoleId], invitedBy: identity, locale: "en",
});
// On your /invite/[token] page, after the person signs in:
const { membership } = await invitations.accept({ token, identity, verifiedEmail: user.email }); // provider-verified e-mail only
Delivery never fails the request: transient SMTP errors are retried with jittered backoff and each attempt has a timeout; the outcome is recorded on the invitation and a failed one can be resend-ed (which issues a new link and invalidates the old one). The Owner role can't be granted by invitation. Without a sender the invitation is still created and the link is returned for you to deliver. resend and revoke take { organizationId, invitationId, actor } and treat another organization's invitation as missing; accept needs a provider-verified address (toVerifiedEmail in each identity adapter).
The two public pages of the flow come ready-made. In Express (@uniora/express):
import { acceptInvitation, invitationPreview } from "@uniora/express";
app.get("/invite/:token", invitationPreview(invitations, { token: (req) => req.params.token }));
app.post("/invite/:token/accept", acceptInvitation(invitations, {
token: (req) => req.params.token,
resolve: async (req) => req.user && { identity: req.identity, verifiedEmail: req.user.verifiedEmail },
}));
In Next.js, previewInvitationRoute(invitations, token) and acceptInvitationRoute(invitations, { token, caller }) (@uniora/next) return a Response for your Route Handlers. Every way an accept can fail answers the same generic 400 invalid_invitation, so the endpoint can't be used to probe which links or e-mails exist. Studio has an Invitations tab to invite, resend and revoke (set UNIORA_INVITE_URL), and npx uniora doctor validates your UNIORA_SMTP_*.
Ownership has two audited, atomic helpers: transferOwnership(storage, { organizationId, fromMembershipId, toMembershipId, actor }) and leaveOrganization(storage, { organizationId, identity }) (the last Owner can never leave). Neither authorizes its caller: guard them yourself.
Hardening: the audit log is append-only and hash-chained (auditLogs.verifyIntegrity(), npx uniora doctor), and the engine can report every decision through onDecision. See guides/hardening.md for the controls that live outside the code (database roles, anchoring the audit head, release settings).
# .env
UNIORA_SMTP_HOST=smtp.example.com
UNIORA_SMTP_PORT=587
UNIORA_SMTP_USER=...
UNIORA_SMTP_PASS=...
UNIORA_SMTP_FROM="Acme <[email protected]>"
@uniora/sqlite implements the same UnioraStorage as @uniora/postgres — and is held to the same behavioural test suite, including the concurrency and security regressions — for local development, tests, and small self-hosted or single-node apps.
npm install @uniora/sqlite better-sqlite3
npm install -D @types/better-sqlite3 # TypeScript only
import Database from "better-sqlite3";
import { applyMigrations, createSqliteStorage } from "@uniora/sqlite";
const db = new Database("app.db");
db.pragma("journal_mode = WAL"); // recommended for a file shared by several processes
applyMigrations(db); // synchronous and idempotent — safe on every start
const storage = createSqliteStorage(db);
Things worth knowing:
begin immediate transaction, so another process on the same file waits its turn instead of racing it. Foreign keys are switched on — and verified — for the connection.ILIKE-style searches fold Unicode case, matching the Postgres adapter.Requires Node.js, pnpm, and Docker (for a local Postgres).
git clone https://github.com/CodexSploitx/uniora.git
cd uniora
pnpm install
docker compose up -d # Postgres on localhost:55432
cp .env.example .env # set DATABASE_URL if you changed the defaults
pnpm build # build every package
pnpm typecheck # typecheck the whole monorepo
pnpm test # run every test suite, including real Postgres and SQLite integration tests
Install it in your project (this puts the uniora command on your PATH for npx):
npm install --save-dev @uniora/cli
or run it once without installing anything: npx @uniora/cli init. (Use the scoped name for that one-off form — a bare npx uniora outside a project that already has @uniora/cli installed would resolve to a different npm package.)
npx uniora init # scaffold uniora.config.mjs + .env.example (--provider sqlite for a SQLite project)
npx uniora check # validate config and the database connection
npx uniora migrate # apply pending uniora.* migrations (tracked in a ledger, additive, never destructive)
npx uniora doctor # deeper diagnostics: Node, .gitignore, config, DB, engine version, migrations, owners, Studio
npx uniora studio # open UNIORA Studio locally (--read-only, --port N, --no-open)
Built to run in CI as well as on your laptop:
npx uniora migrate --status --env production # exits 1 if migrations are pending or were edited after applying
npx uniora migrate --dry-run # what would `migrate` do? touches nothing
npx uniora doctor --json | jq '.checks[] | select(.severity != "ok")'
PostgreSQL or SQLite. database.provider in uniora.config.mjs picks the engine, and database.url is a connection string for "postgresql" or sqlite:<path> for "sqlite" (relative to your project, or absolute):
export default defineConfig({
database: { provider: "sqlite", url: process.env.DATABASE_URL }, // DATABASE_URL=sqlite:./uniora.db
});
With SQLite, the read-only commands (check, doctor, migrate --status/--dry-run) open the file read-only and never create or alter it — a missing file is reported, not conjured; only migrate creates it (and switches it to WAL). init --provider sqlite also adds uniora.db* to .gitignore, since the file holds your authorization data. Studio shows which engine it is on and works the same way, including --read-only (the SQLite connection itself is then read-only).
Every command accepts --config <file>, --env <name> (loads .env.<name> — and only that, never silently falling back to your dev .env) and --json (a single JSON object on stdout, never containing your connection string). Exit codes: 0 ok · 1 something failed · 2 bad usage. npx uniora <command> --help for details.
UNIORA Studio is a local admin UI over your own database — a roles-and-permissions editor, members, per-organization features, a global permission/feature catalog and an append-only activity log. It only listens on 127.0.0.1, is unlocked by a random per-launch token, and never sends data anywhere. Every change it makes is audited; --read-only turns it into a safe viewer.
<Can>, <Feature>) are UX sugar, never a security boundary.userId + organizationId is never treated as sufficient on its own.UNIORA is stable (1.x) and ready for production. Published on npm under the @uniora scope. It follows SemVer: within 1.x the HTTP API, the embedded API and the database migrations only change in a backward-compatible way, and anything that breaks waits for 2.0 and is announced in the CHANGELOG. It keeps improving: see the roadmap.
Issues and pull requests are welcome — this is early-stage, so open a discussion first for anything beyond a small fix. See CONTRIBUTING.md, the roadmap, the changelog and SECURITY.md (report vulnerabilities privately). A runnable app lives in examples/express-sqlite.
UNIORA is open source under the Apache License 2.0 — an OSI-approved license.
NOTICE file when you redistribute it, and say which files you changed.That's a plain-language summary; the LICENSE file is the only thing that governs.
@uniora/auth0| Identity adapter for Auth0. |
@uniora/react | Headless React helpers (<Can>, <Feature>, useCan, useFeature) over a server-computed AuthorizationSnapshot. |
@uniora/next | Next.js glue: request-scoped memoization (react.cache()) plus assertCan/assertAccess/authorizeRoute guards for Server Actions and Route Handlers and the invitation route helpers. |
@uniora/express | Express middleware (requirePermission, requireFeature, authorize): deny-by-default route guards that answer 401/403 and fail closed on any error, plus the invitation preview/accept routes. |
@uniora/server | UNIORA as its own self-hosted HTTP API: API keys with scopes and organization lists, decisions, delegated changes on behalf of an end user, sign-up, invitations; documented endpoint by endpoint (guide). |
@uniora/client | The typed client of that API for your backend, with retries and a remote AuthorizationEngine for the Express and Next guards. |
@uniora/cli | npx uniora init / check / migrate / doctor / studio — with a migration ledger, --json output and CI-friendly exit codes. |
@uniora/studio | UNIORA Studio: a local-first admin UI (Next.js + shadcn/ui + ReUI) to browse and manage organizations, members, roles, permissions, features and the audit log. Launched with npx uniora studio. |