ProcessN — task management
ProcessN is the example's task and workflow app: who is assigned what, by when, and whether it's done. Overdue and due-soon assignments are what drive the urgency ranking on the Portfolio and the "needs you" list the proactive agent surfaces.
It is project-scoped: every ProcessN page lives under /project/$projectKey, so you
are always looking at one building's tasks. (The global surfaces — the
home, Carola, universal search, and the
admin area — aren't project-scoped.)
Coming from Django? Think of ProcessN as one Django app with
Project,Task, andAssignedTaskmodels anddjango-guardianobject permissions — except the "views" are server functions and the permission rules are a typed predicate registry. See Coming from Django.
Where it lives
The logic lives in the @tikab-interactive/pulsn-apps package
(/processn subpath); the example is a thin consumer that wires transport, i18n and routing.
| Layer | Where it lives |
|---|---|
| Logic (package) | @tikab-interactive/pulsn-apps/processn — schema, permissions (typescript-rules), row-filters, server logic, cards |
| Transport | thin createServerFn wrappers in src/lib/processn-server.ts (inject db, map PermissionError → 403) |
| UI | fusion-ui's ResourceTablePage — the example wraps it for labels + routing |
| Routes (pages) | src/routes/_authed.project.$projectKey.processn.assignments.tsx · admin: src/routes/_authed.admin.processn.*.tsx |
Data model
processn_project— the container (a building). Carries the routekey,name,number, and the map coordinates (latitude/longitude).processn_projectuser— membership. The permission booleans (canChangeProject,canAssignTasks) are denormalised onto the row so a visibility query is one join.processn_task→processn_assignedtask— a task definition and the instances assigned to people, each with a status and a deadline.
Pages
| URL | What it shows |
|---|---|
/project/$projectKey | the building overview — Carola plus the single most urgent ProcessN item |
/project/$projectKey/processn/assignments | the viewer's assignments for this building — a ResourceTablePage with status + deadline, editable when the viewer canChange |
/admin/processn/* | site-admin CRUD over projects, tasks, roles (Admin) |
How a page loads
The assignments page is a textbook loader → server function:
export const Route = createFileRoute("/_authed/project/$projectKey/processn/assignments")({
loader: ({ params }) => listProjectAssignments({ data: { projectKey: params.projectKey } }),
component: ProjectAssignmentsPage,
});The component, ProjectAssignmentsPage, reads the loaded rows with Route.useLoaderData()
and hands them to ResourceTablePage — each row carries a canChange flag, and the Edit
button is gated on it.
Permissions
ProcessN is the reference implementation of the example's three-layer permission model
(Authorization). For a member with the Projektör (editor) role:
- SQL filter —
visibleProcessnProjectsWhere(user)in theWHEREclause, so the database only ever returns rows the viewer may see. - Per-object check — the typed registry in
src/permissions/processn.tsdecidescanView/canChangefor each row, and the server tags every returned row with thecanChangeboolean the UI reads. - Re-check on write — a
POSTserver function re-runs the same check before mutating, so a tampered client flag changes nothing.
The whole listProcessnProjects is exactly those first two layers — the SQL filter on the
query, then a loop that attaches each row's canChange:
export async function listProcessnProjects(
ctx: AppCtx,
viewerId: string,
): Promise<ProcessnProjectRow[]> {
const { db } = ctx;
const filters = makeProcessnFilters(db);
// Visibility is filtered in SQL; the loop only attaches per-row canChange.
const projects = await db
.select()
.from(processnProject)
.where(filters.visibleProjectsWhere({ id: viewerId }))
.orderBy(processnProject.name);
const rows: ProcessnProjectRow[] = [];
for (const p of projects) {
const perms = await getProjectPermissions(db, viewerId, p.id);
rows.push({
id: p.id,
name: p.name,
description: p.description,
canChange: perms.canChange,
});
}
return rows;
}The registry itself is composable predicates:
export const processnRegistry = new Registry<User, ProjectContext>();
processnRegistry.add("project.view", isMember);
processnRegistry.add("project.change", isMember.and(memberCanChange));
processnRegistry.add("project.assignTasks", isMember.and(memberCanAssignTasks));Extending it
- A new ProcessN page → copy
_authed.project.$projectKey.processn.assignments.tsxand its server function; follow Building a feature. - A new editable column → add it to the
ResourceTablePagecolumns + the row type, and return it from the loader's server function (with acanChangealready in place). - A new permission → add a predicate to
processnRegistryand surface its boolean on the row; never gate on the client alone — re-check in the write path.
See it populated by bun run db:seed — the seed gives every
Tikab login real assignments (each with at least one overdue and one due-soon task).