Taruvi Refine frontend
Use @taruvi/refine-providers to connect Refine.dev admin UIs to Taruvi. This skill covers the six providers, how to wire resources, the meta vocabulary that unlocks Taruvi-specific features (populate, aggregate, graph, upsert), and the gotchas that aren't obvious from the package API alone.
This skill is the frontend layer. If you're provisioning backend resources, switch to taruvi-backend-provisioning. If you're writing Python for a Taruvi function body, switch to taruvi-functions.
Core principles
- Resource name = datatable name by default. Override with
meta.tableNamewhen they must differ (e.g.,resource: "active_users"queriestableName: "users"with a filter). - Meta is the control surface. Refine's
metaobject is how you reach Taruvi-specific features (populate, aggregate, graph, bucketName, idColumnName, upsert, deleteByFilter). Don't try to push these through filters or query params. - Multiple providers, one client. Register all six providers against the same
Clientinstance. Select providers bydataProviderNamein hooks. - Auth is redirect-based. There is no credentials login in the default Refine flow.
authProvider.login()redirects to Taruvi's login endpoint. - AccessControl batches. Permission checks are debounced 50ms via DataLoader. Tests that don't
awaitwill flake.
Setup
import { Refine } from "@refinedev/core";
import { Client } from "@taruvi/sdk";
import {
dataProvider,
storageDataProvider,
appDataProvider,
userDataProvider,
authProvider,
accessControlProvider,
} from "@taruvi/refine-providers";
const client = new Client({
apiKey: process.env.REACT_APP_TARUVI_KEY!,
appSlug: process.env.REACT_APP_TARUVI_APP!,
apiUrl: process.env.REACT_APP_TARUVI_API_URL!,
});
export function App() {
return (
<Refine
dataProvider={{
default: dataProvider(client),
storage: storageDataProvider(client),
app: appDataProvider(client),
user: userDataProvider(client),
}}
authProvider={authProvider(client)}
accessControlProvider={accessControlProvider(client)}
resources={[
{ name: "posts", list: "/posts", create: "/posts/create", edit: "/posts/edit/:id" },
{ name: "authors" },
]}
>
{/* Routes, layouts, pages */}
</Refine>
);
}
The six providers
1. dataProvider (default)
CRUD against Taruvi datatables. Covers getList, getOne, getMany, create, createMany, update, updateMany, deleteOne, deleteMany, custom.
See references/data-provider.md for full API.
Filter operators (references/filter-operators.md): 24 supported — eq, ne, lt/lte/gt/gte, in/nin, contains/containss (case-insensitive), startswith/endswith (plus s suffix for case-insensitive), negated forms (ncontains, nstartswith, etc.), between/nbetween, null/nnull.
2. storageDataProvider
Buckets and objects. Resource name = bucket name (or override via meta.bucketName).
getList— list files in bucket, supports size/date/prefix filters.getOne— download file as Blob (default) or get metadata (meta.metadata: true).create— upload files:values: { files: File[], paths?: string[], metadatas?: object[] }.update— update file metadata.deleteOne/deleteMany— delete by path.
See references/provider-quickref.md.
3. appDataProvider
App-level resources: roles, settings, secrets; plus useCustom for function and analytics execution.
Supported resources:
getList("roles")— list app roles.getList("secrets")— batch fetch bymeta.keys: string[].getOne("settings")— app settings.getOne("secrets", key)— single secret.
useCustom for execution:
meta.kind: "function"→ executesurlas function slug.meta.kind: "analytics"→ executesurlas analytics query slug.
4. userDataProvider
Users, roles (per user), user-apps. resource: "users" with id: "me" fetches current user. resource: "roles" with meta.username fetches that user's roles. resource: "apps" with meta.username fetches accessible apps.
5. authProvider
Refine AuthProvider interface backed by Taruvi's OAuth redirect flow.
login(callbackUrl?)— redirects to Taruvi login endpoint. No credentials flow.logout(callbackUrl?)— clears cached user, redirects.check()— token presence check (local); server validation happens on API call failure.register(callbackUrl?)— redirect to signup.getIdentity()— current user, cached to_cachedUser.getPermissions()— reuses_cachedUserto avoid a second call.onError(error)— 401 → logout + redirect to/login; 403 → surface error only.
See references/auth-access-control.md.
6. accessControlProvider
Cerbos permission checks via Taruvi Policy. DataLoader batches checks within a 50ms window (configurable via options.batchDelayMs).
const { data: canCreate } = useCan({
resource: "posts",
action: "create",
});
Returns { can: boolean, reason?: string }. Falls back gracefully if user is unauthenticated.
The meta vocabulary
See references/meta-options-cookbook.md for the full list.
Quick reference:
| Meta field | Purpose | Example |
|---|---|---|
populate |
Expand FK relations | "author,category" or "*" or ["author", "comments"] |
select |
Field projection (return only listed fields) | ["id", "total", "status"] |
headers |
Custom request headers | { "X-Request-ID": "..." } |
idColumnName |
Override default "id" PK |
"user_id" |
tableName |
Override resource → table mapping | "users" (when resource is "active_users") |
bucketName |
Override resource → bucket mapping (storage) | "user-uploads" |
aggregate |
Aggregate expressions | ["sum(total)", "count(*)"] |
groupBy |
Group-by fields | ["status", "category"] |
having |
Filters on aggregates | [{field: "sum(total)", operator: "gte", value: 1000}] |
format |
Graph format | "tree" | "graph" |
include |
Graph direction | "descendants" | "ancestors" | "both" |
depth |
Graph traversal depth | 3 |
graph_types |
Filter edge types in graph | ["parent_of", "related"] |
upsert |
Use upsert on create | true |
deleteByFilter |
Delete matching filters | true (plus meta.filters) |
allowedActions |
Request per-row permissions | ["update", "delete"] |
metadata |
Storage: return metadata vs blob | true |
kind |
App provider: function vs analytics | "function" | "analytics" |
keys |
App provider secrets: which to fetch | ["STRIPE_KEY", "API_URL"] |
username |
User provider: for roles/apps | "alice" |
Common patterns
List with populate and filters
const { data, isLoading } = useList({
resource: "posts",
filters: [
{ field: "status", operator: "eq", value: "published" },
{ field: "author_id", operator: "in", value: [1, 2, 3] },
],
sorters: [{ field: "created_at", order: "desc" }],
pagination: { currentPage: 1, pageSize: 20 },
meta: {
populate: ["author", "category"],
},
});
Upsert on create
const { mutate } = useCreate();
mutate({
resource: "users",
values: { id: 42, email: "new@example.com" },
meta: { upsert: true },
});
Graph traversal
const { data } = useList({
resource: "categories",
meta: {
format: "tree",
include: "descendants",
depth: 3,
},
});
Aggregation
const { data } = useList({
resource: "orders",
meta: {
aggregate: ["sum(total)", "count(*)"],
groupBy: ["status"],
having: [{ field: "sum(total)", operator: "gte", value: 1000 }],
},
});
File upload
const { mutate } = useCreate();
mutate({
dataProviderName: "storage",
resource: "user-uploads",
values: {
files: [file1, file2],
paths: [`${userId}/avatar.png`, `${userId}/cover.png`],
metadatas: [{ kind: "avatar" }, { kind: "cover" }],
},
});
Execute a function from the UI
const { refetch: sendEmail } = useCustom({
dataProviderName: "app",
url: "send-welcome-email",
method: "post",
config: { payload: { user_id: userId } },
meta: { kind: "function" },
queryOptions: { enabled: false },
});
Access control on a button
const { data: canDelete } = useCan({
resource: "posts",
action: "delete",
params: { id: postId },
});
return canDelete?.can ? <DeleteButton id={postId} /> : null;
Gotchas
meta.idColumnNamedefaults to"id". Tables with a different PK must passidColumnNameon every hook that needs it (useOne,useUpdate,useDelete).- Resource vs table name. Refine's resource name goes straight to Taruvi unless
meta.tableNameoverrides. If you name your resourceactive-usersbut the table isusers, you'll get a 404. _cachedUserstaleness. After a role change, the cached user is stale until logout or page refresh. Force re-fetch viauseGetIdentitywithqueryOptions: { refetchOnMount: true }if you need fresh permissions.- 401 vs 403 onError semantics. 401 triggers logout + redirect; 403 does not. Handle 403 UX yourself (toast, error page).
authProvider.login()withoutcallbackUrlgoes to the default callback. Pass one if you want to land back on the originating page.useCanfires for every row. A list of 100 rows callinguseCanper row → 100 batched checks in one request via DataLoader, but that first render is 50ms delayed. Test withawaitin tests.useCustomwithmeta.kind: "function"ignoresmethod. Functions are always POST under the hood.- Storage
getOnereturns a Blob by default. Passmeta.metadata: trueto get the file's metadata object instead. - Graph format="tree" requires
hierarchy.enabledon the table. Graph format requiresgraph.enabled. See the backend-provisioning skill for schema setup. user-invocable: false-style skills don't apply here — this is a client-side library, not an agent skill runtime. Don't confuse consumer app configuration with agent skill metadata.
Verification checklist
Before reporting a frontend feature as done:
- Every new Refine resource is registered in the
<Refine resources={[...]}>array with at least aname. - Resources whose name doesn't match the Taruvi datatable have
meta.tableNameset on every hook — or the resource is renamed to match. - Tables with non-
idprimary keys passmeta.idColumnNameon every hook (useOne,useUpdate,useDelete,useUpdateMany). - Dashboard / KPI widgets aggregate server-side (
meta.aggregate/meta.groupByfor single-table, oruseCustomwithmeta.kind: "analytics"for multi-table) — never full row fetches derived in React. - List pages use backend pagination, server-side filters, server-side sort. No client-side filtering of backend results.
-
useCanormeta.allowedActionsgates the right UI elements; 403 vs 401 errors render as expected (403 stays on page, 401 redirects to login). - Storage batch uploads surface the all-or-nothing failure mode correctly in UX; batch deletes surface per-file status.
- Notifications go through Refine's
notificationProvider— no ad-hoc toast libraries. -
node scripts/validate-resource-map.js <path-to-app>passes (or was skipped with a documented reason).
When you get stuck
- Full API reference for the default provider: references/data-provider.md.
- Every filter operator: references/filter-operators.md.
- All
metaoptions in context: references/meta-options-cookbook.md. - Auth + access control details: references/auth-access-control.md.
- Direct
@taruvi/sdkusage when providers don't fit: references/sdk-primer.md. - Exported types and utility functions: references/types-and-utilities.md.
- Validate Refine resources match Taruvi datatables:
node scripts/validate-resource-map.js(see script header for usage).