User guide · 11 of 11

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).

Inviting users

Two kinds of invite code work at registration:

  • 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 — a master fallback that always works alongside minted codes. To rotate it, change the env value and restart the app; existing users are unaffected.

Prefer minted codes for anyone outside your inner circle: they're individually revocable and you can see exactly who came in on what.

Invite requests

Visitors without a code can request one from the register page (email plus an optional note). Requests appear in the Invite requests section of /admin:

  • Approve mints a one-use code and emails it to the requester with a prefilled register link.
  • Decline is silent — the requester is not notified — and reversible: if they request again later, the request reappears as pending.

The requester always sees the same generic "if an invite becomes available…" message, so the form never reveals whether an email is known.

The user directory (/admin)

Sortable table of every registered user: 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 or a missing INVITE_CODE.
  • Data-protection contact: DATA_CONTACT_EMAIL appears in the footer and privacy policy — make sure it's a mailbox someone reads.