Skip to content
Fusion

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, and AssignedTask models and django-guardian object 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.

LayerWhere it lives
Logic (package)@tikab-interactive/pulsn-apps/processn — schema, permissions (typescript-rules), row-filters, server logic, cards
Transportthin createServerFn wrappers in src/lib/processn-server.ts (inject db, map PermissionError → 403)
UIfusion-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

Loading diagram...
  • processn_project — the container (a building). Carries the route key, 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_taskprocessn_assignedtask — a task definition and the instances assigned to people, each with a status and a deadline.

Pages

URLWhat it shows
/project/$projectKeythe building overview — Carola plus the single most urgent ProcessN item
/project/$projectKey/processn/assignmentsthe 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:

example/src/routes/_authed.project.$projectKey.processn.assignments.tsx
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:

  1. SQL filtervisibleProcessnProjectsWhere(user) in the WHERE clause, so the database only ever returns rows the viewer may see.
  2. Per-object check — the typed registry in src/permissions/processn.ts decides canView / canChange for each row, and the server tags every returned row with the canChange boolean the UI reads.
  3. Re-check on write — a POST server 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:

pulsn-apps/src/processn/index.ts
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:

pulsn-apps/src/processn/permissions.ts
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.tsx and its server function; follow Building a feature.
  • A new editable column → add it to the ResourceTablePage columns + the row type, and return it from the loader's server function (with a canChange already in place).
  • A new permission → add a predicate to processnRegistry and 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).