This repository is a synthetic prototype for a reusable foundation for internal tools. It includes sign-in, roles, server-side authorization, audit logging, and a review queue.
It answers this question: can a team build internal tools on the same secure base instead of copying application infrastructure or relying only on Power Apps? The intended value is that every new internal tool starts from the same base and adding one is fast.
Prerequisites:
- Node 24 LTS
- npm
Run:
git clone https://github.com/hessabi/internal-tools-devin-poc.git
cd internal-tools-devin-poc
npm run setup
npm run devOpen http://localhost:3000.
Useful checks:
npm run typecheck
npm run lint
npm testThe setup command creates a local .env from .env.example when needed,
installs dependencies, creates the SQLite database, and loads seed data. All
seeded users and records are synthetic.
| Name | Role | |
|---|---|---|
| Test Analyst 01 | analyst.one@example.com | analyst |
| Test Analyst 02 | analyst.two@example.com | analyst |
| Test Manager 01 | manager.one@example.com | manager |
| Test Admin 01 | admin.one@example.com | admin |
Development sign-in is gated by AUTH_DEV_LOGIN. It is disabled when
NODE_ENV=production.
The AuthProvider interface in src/lib/auth/provider.ts separates identity
from the application. The local DevAuthProvider uses a signed, httpOnly
cookie. A real OIDC provider can replace it at the provider selection point
in src/lib/auth/index.ts.
The permission matrix is:
| Capability | analyst | manager | admin |
|---|---|---|---|
| View KYC queue and case detail | yes | yes | yes |
| Start review, edit notes before a decision | yes | yes | yes |
| Approve or reject low or medium risk | yes | yes | yes |
| Approve or reject high risk | no | yes | yes |
| View refunds dashboard and refund detail | yes | yes | yes |
| Start a refund review | yes | yes | yes |
| Approve or reject refunds of 500 USD or less | yes | yes | yes |
| Approve or reject refunds over 500 USD | no | yes | yes |
| View audit log | no | yes | yes |
Checks run in server code. A case or refund cannot be approved or rejected by its assignee. This maker-checker rule applies even when the actor has the required role. Rejecting always requires a comment. High-risk KYC decisions and refund decisions above the threshold require a manager or admin.
The audit log stores the actor, action, entity type, entity ID, changed fields,
optional comment, and UTC creation time. It stores only fields whose values
changed between the before and after records. Customer email and updatedAt
are redacted from changes. A Prisma query extension rejects audit updates and
deletes, so entries are append-only through the application client.
src/lib/review-queue/ provides shared validation, role checks, pagination,
transition execution, audit writes, and UI components. Each tool supplies a
typed configuration, a repository, thin server actions, and two pages.
This is a trimmed excerpt from the real KYC configuration in
src/apps/kyc/config.tsx:
const kycQueue = defineReviewQueue({
key: "kyc",
basePath: "/kyc",
entityType: "kyc_case",
readRoles: ROLES,
transitions: [
{
action: "approve",
from: ["in_review"],
to: "approved",
allowedRoles: approvalRoles,
guard: notAssignee,
},
],
filters: [
{ key: "status", options: statusOptions },
{ key: "riskLevel", options: riskOptions },
],
repository: kycRepository,
});Both tools are registered in src/apps/registry.ts. The home page, the
header navigation, and the audit log entity links read from that list, so a
new tool appears in all three once it is registered.
- KYC review queue (
src/apps/kyc/,/kyc): analysts review synthetic customer cases; high-risk decisions need a manager or admin. - Refunds dashboard (
src/apps/refunds/,/refunds): support agents review synthetic refund requests. Refunds overREFUND_MANAGER_THRESHOLD_CENTS(500 USD, insrc/apps/refunds/limits.ts) need a manager or admin. Amounts are stored as integer cents. The order reference is treated as sensitive: it appears on the detail page but is redacted from audit changes and left out of the list. Out of scope for the first version: sending refunds to a payment processor, customer notifications, links to real orders, partial refunds, and currencies other than USD.
List requests use a default page size of 20 and a maximum page size of 50.
Filter keys and values are checked with Zod at the server boundary. Errors use
typed application codes such as FORBIDDEN, VALIDATION,
INVALID_TRANSITION, and COMMENT_REQUIRED.
The example below is a filled plan for a fictional Expense claims tool.
Name and purpose: Expense claims. Review employee expense claims and approve payment.
Who uses it: Analysts review claims, managers approve claims over 500, and admins can approve all claims and investigate audit entries. Any new role must be named here before implementation.
The record and its fields:
| Field | Type | Example value | Sensitive? |
|---|---|---|---|
| claim label | text | Expense claim EC-001 | no |
| claimant label | text | Test Employee 001 | no |
| amount | number | 640.00 | no |
| currency | text | USD | no |
| receipt reference | text | test-receipt-001 | yes |
| notes | long text | Hotel receipt attached | no |
States:
| State | Meaning | Example |
|---|---|---|
| pending | Ready for review | Expense claim EC-001 is pending |
| in review | Someone is reviewing it | Expense claim EC-001 is in review |
| approved | Ready for payment | Expense claim EC-001 is approved |
| rejected | Needs correction or cannot be paid | Expense claim EC-001 is rejected |
Transitions:
| From | To | Action name | Who can do it | Thresholds or risk rules | Comment required? |
|---|---|---|---|---|---|
| pending | in review | start_review | analyst, manager, admin | Expense claim EC-001 is assigned to the actor | no |
| in review | approved | approve | analyst, manager, admin | Claims over 500 need a manager; the assignee cannot approve | no |
| in review | rejected | reject | analyst, manager, admin | Claims over 500 need a manager; the assignee cannot reject | yes |
Filters for the list:
| Filter | Options | Example |
|---|---|---|
| status | pending, in review, approved, rejected | Expense claim EC-001: approved |
| amount band | 0 to 500, over 500 | Expense claim EC-001: over 500 |
| currency | USD, EUR | Expense claim EC-001: USD |
Anything out of scope: Notifications, payment execution, and receipt file storage are out of scope for the first version.
Build every page from the shared shadcn/ui components in src/components/ui/ (table, badge, button, select, card, dialog) and the app shell in src/components/app-header.tsx, not a per-tool kit.
- Add the record and fields. Add an
ExpenseClaimmodel toprisma/schema.prismawith a named assignee relation onUser, runnpx prisma generateandnpm run db:push, and add synthetic seed rows inprisma/seed.ts. Add the new table to thedeleteManycalls at the top of the seed. Store money as integer minor units (cents) and format it withformatMoneyfromsrc/lib/format.ts. - Mark sensitive fields. Add
receiptReferencetoREDACTED_FIELDSinsrc/lib/audit/record.ts. Do not include it in list columns. - Define states. Reuse
ReviewStatusfor these four states. If the tool needs a different state, extend the shared state schema explicitly and update its labels and transition types. - Define transitions and rules. In
src/apps/expense-claims/config.tsx, add atransitionsarray withallowedRoles,requireComment, asuccessMessageshown after the action succeeds, and thenotAssigneeguard. SetnotesLockedStatusesto the states where notes become read-only. Add a namedCLAIM_MANAGER_THRESHOLD_CENTS = 500_00insrc/apps/expense-claims/limits.ts. Use that constant in theallowedRolesfunction and in the repository filter so the rule and the filter cannot drift apart. Choice fields such as a reason list live in their own file with a Zod enum and labels, likesrc/apps/refunds/reasons.ts. - Define filters. Add the status, amount band, and currency entries to
the
filtersarray in the queue configuration. Keep option values in one configuration source. A derived filter such as amount band is translated into a Prismawhereclause in the repository; seeamountFilterinsrc/apps/refunds/repository.ts. - Add the repository. Create
src/apps/expense-claims/repository.tsto translate queue filters and pagination into Prisma queries. - Add server actions. Create
src/apps/expense-claims/actions.tswith thin actions that call the shared transition and notes functions, then revalidate the route. - Add two thin pages. Create
src/app/expense-claims/page.tsxfor the list andsrc/app/expense-claims/[id]/page.tsxfor the detail view. Both obtain a session and render shared queue components. Read the title andbasePathfrom the queue configuration instead of repeating them. Only render the notes section when the queue setseditableNotesField. - Register the tool. Add the queue configuration to
reviewQueuesinsrc/apps/registry.ts. The home page, the header navigation, and the audit log entity links all read from that list. - Copy the tests. Add a
createExpenseClaimfixture totests/helpers/fixtures.tsand add the new table toresetDatabase. Copy the integration test patterns intests/permissions.test.ts,tests/audit.test.ts,tests/transitions.test.ts,tests/auth.test.ts, andtests/list.test.tsinto onetests/expense-claims.test.ts. Cover the 500 threshold for both approve and reject, maker-checker, required comments, audit changes with sensitive fields redacted, and list filters.tests/refunds.test.tsis a complete example.
The target is one schema model, one queue configuration, one repository file, one actions file, two thin pages, one registry entry, and one test file. Authorization and transition behavior remain shared server logic rather than copied page code.
This prototype does not include a feature-flag panel, real SSO or OIDC, deployment or hosting, notifications, or a custom design system. Shared UI uses shadcn/ui components.
Moving this foundation into production would still require:
- Identity with Conditional Access and session revocation.
- Server-side RBAC with field-level masking.
- Audit retention.
- Secrets and egress controls.
- Backups and restore tests.
- A defined compliance scope.
- Monitoring and on-call.
- Access reviews.
Known risks in this prototype that production must close:
- Audit entries are append-only only in the Prisma client (
src/lib/db.ts). The database must also denyUPDATEandDELETEonAuditEntrythrough grants or triggers. The seed script uses a plain client and can clear the table, which is intended for local data only. - Notes are locked once a case is approved or rejected, but any role can edit them before that. Decide whether some roles or states should be read-only.
- The dev session cookie is an HMAC of the user id with no expiry or revocation. It is acceptable for local sign-in only; the OIDC provider must own session lifetime and revocation.
- Roles, states, and risk levels are plain strings validated by Zod in the app. The database has no check constraints, so a direct write can store an invalid value. Add Postgres enums or check constraints.
These production concerns are described in
docs/power-apps-research.md, section 6.
This prototype was built with Devin Cloud across separate sessions, one per piece of work:
- PR #1: Power Apps build-vs-buy research.
- PR #2: foundation and KYC review queue.
- PR #4: refunds dashboard on the review-queue foundation.
- Issue #3: the refunds dashboard tool request that PR #4 implemented.
AGENTS.md was the guardrails file for every session. It sets the scope,
stack, engineering rules, verification steps, and writing style that each
session had to follow.
prisma/
schema.prisma
seed.ts
src/
app/
page.tsx
sign-in/page.tsx
kyc/page.tsx
kyc/[id]/page.tsx
refunds/page.tsx
refunds/[id]/page.tsx
audit/page.tsx
apps/
registry.ts
kyc/
config.tsx
repository.ts
actions.ts
risk.ts
types.ts
refunds/
config.tsx
repository.ts
actions.ts
limits.ts
reasons.ts
types.ts
lib/
auth/
audit/
config/
review-queue/
tests/
helpers/fixtures.ts
permissions.test.ts
audit.test.ts
transitions.test.ts
auth.test.ts
list.test.ts
notes.test.ts
refunds.test.ts