Architecture
Pidrang is an Nx monorepo of small packages with a strict dependency direction: everything depends on @pidrang/core, and core depends on nothing but @angular/core.
text
@pidrang/core
(contracts, providePidrang, signals)
|
+----------+---------+----------+
| | |
@pidrang/ @pidrang/ @pidrang/
supabase material tailwind
| | |
Supabase Angular Material Tailwind CSSPackages
text
pidrang/
├── apps/
│ ├── demo-material/ Angular demo (Material UI)
│ ├── demo-tailwind/ Angular demo (Tailwind UI)
│ └── pidrang-cli/ the `pidrang` command line entry point
└── libs/
├── core/ headless primitives and contracts (@pidrang/core)
├── supabase/ Supabase DataProvider and AuthProvider (@pidrang/supabase)
├── material/ Angular Material UI layer (@pidrang/material)
├── tailwind/ Tailwind CSS UI layer (@pidrang/tailwind)
├── cli/ provider-agnostic wizard and codegen (@pidrang/cli)
├── cli-supabase/ Supabase schema introspection for the CLI (@pidrang/cli-supabase)
├── provider-conformance/ shared DataProvider contract tests, used by specs only (private)
└── adapter-coexistence/ renders both UI layers in one component, test only (private)@pidrang/core
Framework-native (Angular) but UI-agnostic. It depends on @angular/core only, never on a component library, @angular/common, @angular/forms or @angular/router. Subpaths of @angular/core are allowed, rxjs is not. That boundary is what lets any UI layer or data source be added without touching core.
Resource(optionally described withFieldandFieldTypefor a typed form or table),DataProvider,ListParams(with typedFilters:normalizeFilters,validateListParams,rejectUnsupportedFilters) andListResult<T>,GetOneParams,CreateParams<T>,UpdateParams<T>,DeleteParamsprovidePidrang({ dataProvider, resources, authProvider? })for app-wide configurationinjectDataProvider(),injectAuthProvider(),injectResources(),injectResource(name)for reading that configurationinjectResourceList<T>(resourceName, params?), a signal-based list loader built on Angular's nativeresource()injectAuthSession(), a signal-based view of the current identity, returned as anAuthSessionRefPidrangError, the error type data and auth providers throw, with acategory(unauthenticated,forbidden,not-found,unknown) pluscode,statusandcause
AuthProvider (login, logout, getIdentity, optional onSessionChange) follows the same pattern as DataProvider: providePidrang registers it, injectAuthProvider() reads it, and injectAuthSession() returns an AuthSessionRef built on resource(). Its value() is AuthIdentity | null | undefined (signed in, signed out, not loaded yet), keeps the previous identity while a session change reloads it, and never throws: a provider failure goes to error() and leaves value() undefined. A provider that implements onSessionChange(listener) makes every live injectAuthSession() reload on login, logout or an external auth change, and unsubscribes it when destroyed. reload() also works for providers without it.
@pidrang/supabase
Implements DataProvider and AuthProvider against @supabase/supabase-js:
SupabaseDataProviderandprovideSupabaseDataProvider({ client })mapListParamsto PostgREST filters (eq,neq,lt,lte,gt,gte, an escapedinlist,imatchwith an escaped pattern forcontains,is),.order()and.range(), reject a filter field that PostgREST would read as a query option or a path (order,limit,a.b,a->b) and a sorter field holding path syntax, and normalizePostgrestErrorto aPidrangErrorwhosecategorycomes from the HTTP status (except 42883, a missing operator, which staysunknown), so an app can ask for sign-in onunauthenticatedonly, never on a signed-in user's Row Level Security denial.getListalways addsidascending as a final sort, unless a sorter already targetsid, so pages never repeat or skip rows.SupabaseAuthProviderandprovideSupabaseAuthProvider({ client })wrap Supabase Auth. A missing session (AuthSessionMissingError,session_not_found) is anullidentity; network and server failures are thrown asPidrangError. Supabase auth events driveonSessionChange, and a repeatedSIGNED_INfor the same user (sent when a tab regains focus) is ignored.
Both factories return plain objects and do not use Angular DI. The app wires them in through providePidrang.
Every table is assumed to have an id primary key: getOne, update and delete filter on it.
Access control is your responsibility
SupabaseDataProvider and SupabaseAuthProvider perform no authorization. Every access guarantee comes from the Row Level Security policies on your own Supabase project. Pidrang assumes those policies are correctly configured; it does not enforce or verify them.
UI layers
@pidrang/material and @pidrang/tailwind expose the same schema-driven components:
ResourceTablerenders aResource'sField[]as a table. A projected#actionstemplate (contentChildandngTemplateOutlet) becomes a trailing column for per-row buttons and links.ResourceFormrenders one input perField, built on Angular Signal Forms (@angular/forms/signals). AlinkedSignaldraft resets whenever itsvalueinput changes and never mutates the caller's original object.
They depend on @pidrang/core and their own styling stack, never on a specific DataProvider. They therefore work with @pidrang/supabase today and any future data provider unchanged.
@pidrang/tailwind ships no CSS. The consuming app's Tailwind build, through its own @source scan, turns the classes used in the library's templates into styles.
CLI
@pidrang/cli is the provider-agnostic engine behind pidrang init: the interactive wizard, the Resource codegen and the SchemaProvider contract every schema source implements. @pidrang/cli-supabase implements it for Supabase with three connection strategies: GraphQL introspection (pg_graphql), the REST OpenAPI document (service role key) and direct Postgres (information_schema). apps/pidrang-cli wires the two together.
Adding a data source or UI package
New packages such as @pidrang/ng-zorro, @pidrang/rest, @pidrang/graphql depend on @pidrang/core only. If adding one requires changing core, treat that as a design smell and re-examine the core contract first.
The reasons behind non-obvious implementation choices are in Design notes. The data provider contract, with its filter semantics and conformance suite, is in Writing a data provider.
