Persfina

User guide · 12 of 12

Administrator guide

For whoever operates the Persfina server. Deployment itself is covered by the Deployment section of the project README; this page covers day-to-day administration.

Becoming an admin

There is no admin signup. Register normally, then promote the account on the server:

node scripts/promote-admin.mjs <email>

Admin rights are checked against the database on every request, so promoting or demoting takes effect immediately — no re-login needed. Admins get an Admin area (separate layout, "Exit admin" returns to the normal app).

Invite codes

Registration is open — no code is needed to create an account. Invite codes still exist for provenance: a code entered at registration (or carried by an invite link) records which code admitted the user. Two kinds work:

  • Minted codes — created in the Invite codes section of /admin: set an optional label ("for James") and a use count (1–100), and Persfina generates a unique code plus a shareable link (/register?code=…) that arrives with the code prefilled. Each registration consumes one use; exhausted codes stop working automatically, and you can revoke an unused or partly-used code at any time. The table shows every code's uses, status, and creation date (labels are editable later), and each user's record remembers which code admitted them.
  • The shared INVITE_CODE from the server's .env — optional; if set, it works alongside minted codes. To rotate it, change the env value and restart the app; existing users are unaffected.

Prefer minted codes when you want attribution: they're individually revocable and you can see exactly who came in on what.

The user directory (/admin)

Sortable table of every registered user: name and email (admins badged, disabled accounts marked), role, joined date, last login, number of statement uploads, 90-day spend, and top category. Click an email for the drill-down.

Per-user drill-down (/admin/{user})

  • Header: email, role, joined/last-login, account count, verification and disabled status, and which invite code admitted them.
  • Reset password — set a new password (min 8 chars) for a locked-out user. This immediately signs the user out of all their devices and emails them a notice that an administrator reset their password. Tell them the new password through a channel you trust, and ask them to change it in Settings.
  • Verification tooling — resend the verification email or mark the address verified manually (e.g. after confirming it out-of-band).
  • Role — promote a user to admin or demote an admin. You cannot change your own role, and a demotion that would leave no other active admin is refused. Demotion signs the user out everywhere.
  • Marketing preferences — edit the user's opt-ins on their recorded request; every change is written to the consent log with you as the acting admin.
  • Read-only copies of the user's spending dashboard, investments view (if they have trades), their miles balances, and their statement upload history. The admin view cannot refresh prices or modify user data.

Disabling and deleting a user

Deletion is deliberately two-step:

  1. Disable the account — sign-in is blocked and any live sessions stop working immediately. Reversible with one click.
  2. Only a disabled account shows the Delete form, which requires typing the user's email to confirm. This runs the same permanent cascade as self-serve deletion in Settings (all financial data, consent log, in-flight uploads). You cannot disable or delete yourself.

Stats, audit log, and ops

  • /admin/stats — site-wide metrics: users (total/verified/disabled/ admins), invite usage, statements, transactions, and content counts.
  • /admin/audit — a filterable log of every admin action (actor, action, target, metadata, timestamp). Password resets, role changes, disables, deletions, invite approvals, and consent edits all land here.
  • /admin/ops — operational health: the upload store (file counts and ages, so a sweep failure is visible) and quote staleness for the investments price cache.

Email

With RESEND_API_KEY set, Persfina sends verification, password-reset, and admin-reset-notice emails via Resend from EMAIL_FROM. Links in emails use APP_URL — it must be the public HTTPS URL. Without a key (dev), emails are printed to the server console instead.

Operational notes

  • Rate limiting is in-process; restarting the app resets counters.
  • Sessions: revocation works by bumping a user's tokenVersion (automatic on any password change/reset). Rotating SESSION_SECRET signs out everyone at once.
  • Uploads directory self-cleans: PDFs are deleted after import and swept 24 h after upload otherwise. It needs no backup — the database is the system of record; back that up nightly.
  • Production boot checks: the app refuses to start in production with a weak/placeholder SESSION_SECRET.
  • Data-protection contact: DATA_CONTACT_EMAIL is never shown to visitors — it's the inbox that receives /contact form messages. Make sure it's a mailbox someone reads.