Technical guide · 1,906 words
How We Built Dashier: A Schema-Driven Admin Infrastructure
Dashier is built around a simple architectural decision: a data source is not merely a table shown in an admin screen. It is a definition that drives storage, validation, generated UI, and an API contract. That decision lets modern applications manag
Dashier is built around a simple architectural decision: a data source is not merely a table shown in an admin screen. It is a definition that drives storage, validation, generated UI, and an API contract. That decision lets modern applications manage many kinds of operational data without a separate, hand-built dashboard for every resource.
Next.js 15 and TypeScript provide the App Router and Route Handlers; Supabase provides PostgreSQL, Auth, and Storage. The important design work is the boundary between a stable platform core and changing application data: organizations and projects establish tenancy, data-source metadata describes each resource, and records carry values in JSONB.
The hierarchy is the tenancy boundary
Dashier’s load-bearing hierarchy is Organization → Project → Data Source → Record. An organization is the top-level tenant. Projects group an application’s resources, data sources describe those resources, and records are the individual values managed through the generated interface or REST API.
This hierarchy is more than navigation. Core rows carry organization_id directly or inherit organization ownership through an enforced join. A project is resolved under an organization, and a data source is resolved under that project. Every read and write therefore has to preserve the chain rather than trusting an identifier supplied by a browser or API caller.
The database model reflects that responsibility. Core tables include organizations, organization_members, projects, data_sources, data_source_fields, and data_source_records, alongside roles, permissions, API keys, and audit logs. A source has a project, name, slug, and description. Its fields have a key, display name, type, required and uniqueness rules, defaults, options, and ordering. A record has fixed system columns plus a values JSONB payload.
That last choice avoids creating a new physical table every time an application adds a resource. Fixed columns can hold identity, ownership, timestamps, and attribution; the JSONB document holds the source-specific values. The result is a common CRUD engine that can operate on many sources while still keeping source ownership and record identity explicit.
One field definition drives four surfaces
data_source_fields is the source of truth for a data source’s shape. Dashier does not maintain one schema for a form, another for a table, and a third for the API. Instead, the field definitions are resolved into typed field definitions and reused across those surfaces.
The supported field vocabulary includes text, textarea, number, decimal, boolean, email, phone, URL, date, datetime, JSON, image, file, enum, reference, array, and rich text. Relationships cover one-to-one, one-to-many, and many-to-one patterns, such as an event with registrations. Required, nullable, unique, default, option, and position metadata let the engine build behavior instead of treating every field as an unstructured string.
In the generated UI, a source produces a list with search, filtering, sorting, pagination, and bulk actions. The same definition produces create and edit forms, import/export controls, and validation expectations. A field’s type can determine whether the form renders a checkbox, date control, enum selector, or file input; its constraints can determine what is accepted before insertion.
The API uses that same definition when it validates incoming JSON and maps stored values back to a response. This matters when a field changes: the platform has one place to update and inspect the contract. It also makes imports safer because CSV and JSON rows can be previewed and validated against the source before they are inserted. Image and file fields are stored through Supabase Storage, with the resulting Storage URL represented in the record rather than silently changing the record model.
The following is an illustrative field definition, not a claim about a particular customer or a literal database seed:
// Illustrative TypeScript shape for a generated source definition.
const fields = [
{ key: "name", type: "text", required: true, unique: false },
{ key: "status", type: "enum", options: ["active", "paused"] },
{ key: "joined_at", type: "datetime", required: false },
];The useful property is that the same metadata feeds the form renderer, table columns, Zod validation, import preview, and REST handler. A source can therefore be configured rather than implemented repeatedly.
The REST surface stays predictable
Every data source receives a consistent API base path: /api/v1/{data-source-slug}. List and create operations use that path; get, update, and delete operations add /{record-id}. Slugs are the API and UI identifiers, are URL-safe, and are intended to remain stable after creation.
A representative illustrative request shape looks like this:
# Illustrative only: list records from a source.
curl "https://example.invalid/api/v1/members?org=acme&project=portal" \
-H "Authorization: Bearer <api-key>"
# Illustrative only: create a record.
curl -X POST "https://example.invalid/api/v1/members" \
-H "x-api-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{"name":"Example Member","status":"active"}'The route handler does not treat the slug as authorization. It resolves a project-scoped source, loads its field definitions, then evaluates the endpoint policy for the method. Generated routes consequently share one security and validation path.
Authentication and endpoint policy are separate decisions
The current API authentication implementation accepts an API key in either the x-api-key header or an Authorization: Bearer header. The raw key is hashed before lookup. A missing key, invalid key, or revoked key is rejected, while a valid key establishes the organization, project, key identity, and scopes associated with it. The implementation also records the key’s last-used timestamp without making that update part of the request’s main response path.
A caller can instead identify the organization and project with org and project query parameters. The server checks the Supabase session and organization membership, enforces a member’s project allowlist, and can further restrict session access with a source allowlist. Non-matching relationships return a deliberately non-disclosing “unknown” response.
Authentication is then combined with an endpoint’s configured access mode. The supported modes are Public, Authenticated, and Role. Public routes do not require credentials, authenticated routes require a session or a key with the needed scope, and role-protected routes require an allowed session role or an admin-scoped API key. A route can also be disabled or marked as not exposed through the API channel; in either case, valid credentials do not make it reachable.
Scopes keep API keys narrower than an all-purpose credential. For ordinary methods, the resolver maps GET to read and other methods to write, then checks the key’s scopes. Role-protected access is stricter: the key must include admin, while session access compares the member’s role with the endpoint’s allowed role IDs. This combines machine credentials and human membership without pretending they are interchangeable.
Rate limiting is evaluated after source and policy resolution. The identity is the API key when present, otherwise the first forwarded IP value (or an unknown fallback). The endpoint policy supplies the limit; a rejected request receives HTTP 429 with limit, remaining, and retry headers. This is not a claim about a performance target.
Supabase Auth, RLS, and server-side checks form a layered model
Google OAuth is the only sign-in provider in the current scope. Supabase Auth supplies the session identity, but identity alone is not permission. The platform expects Supabase Row Level Security, server-side organization_id scoping, and a permission check on every read and write. Client-supplied roles or permissions are never authoritative.
That layered model addresses different failure modes. RLS protects database access at the data layer. Server-side organization and project resolution keeps a request inside its tenant hierarchy. RBAC decides whether the identified member can perform an action, while endpoint policy decides whether a generated API route is exposed and under which mode. Each layer should reinforce the others instead of standing in for them.
The role model includes Owner, Admin, Editor, and Viewer, plus custom roles and permissions. Permission keys follow {slug}.read, {slug}.write, and {slug}.delete; for example, members.read. A project can therefore express access in terms of configured resources rather than hard-coded application tables.
Validation belongs at the Route Handler boundary. Inputs are expected to be validated with Zod before they reach the persistence layer, using the resolved field definitions to determine what a valid record looks like. File uploads additionally need type and size checks and organization/project-specific Storage boundaries. These are implementation expectations recorded in the project scope, not a claim that every possible storage or security control is already present in the supplied files.
Imports, exports, and audit make the system operational
A schema-driven admin surface is useful only if data can enter and leave it. Dashier’s current scope supports CSV and JSON import with a preview-and-validate step before insertion, plus CSV and JSON export. Previewing first gives operators a chance to see parsing and field-validation problems before they become records. Because the source definition is shared, import validation can follow the same required, type, option, and relationship rules as direct API writes.
Audit logs provide the other side of operational visibility. Create, update, and delete actions, along with role and permission changes, are expected to write an audit entry containing organization, user, action, entity type, entity ID, and old and new values. API-key calls can be associated with a key while session calls can carry the acting user ID; the important invariant is that a mutation is not silently detached from its context.
These capabilities are connected. A CSV import is a series of validated writes that should follow the same organization scope and permission checks as a form submission. An export should resolve the same source and access policy as a list request. A role change should be audited because it changes who can perform future writes.
Why this architecture is useful—and where it stops
The architecture optimizes for a stable admin engine over changing application data. A new source is a metadata task: define fields, relationships, policies, and permissions, then use generated tables, forms, import/export, and REST operations. It still requires sound field definitions and access rules.
Dashier’s documented scope is deliberately specific. It includes generated CRUD, REST, RBAC, files, imports/exports, and audit logs; it does not include GraphQL, workflows or webhooks, a marketplace, an SDK generator, an AI schema builder, white-label custom domains, email/password sign-in, or MySQL/MongoDB support. Those boundaries matter when comparing products: Dashier is not a drop-in replacement for every data platform or internal-tool product.
For context, official pricing pages checked 2026-10-04 show different models elsewhere: Retool pricing lists separate builder and internal-user prices across Free, Team, and Business plans, with Enterprise custom; Directus pricing lists a free Core option with stated caps, paid Team pricing, and custom Enterprise pricing, while also describing self-hosting and eligibility rules; Airtable pricing is seat-based on paid plans and its FAQ says read-only collaborators are not charged under Team and Business, while Airtable’s platform page describes its broader platform. Prices and features change, so these links are reference points, not permanent claims—and the comparison does not imply that Dashier is a drop-in replacement.
Dashier’s approved pricing direction is separate from that architecture: the Founding User plan is $0/month, with everything included for early adopters and free access while Dashier grows; Starter is planned at $5/month and marked Coming Soon; Business is custom with Contact Us. No feature should be inferred to belong to a particular tier from that description.
The central idea is straightforward: store application values as metadata-backed JSONB records, keep field definitions authoritative, resolve requests through the organization and project hierarchy, and reuse definitions for UI, validation, API, and operations. Generated administration is a consistent set of surfaces built from one schema and guarded by explicit tenancy, authentication, policy, and audit boundaries.