andré santos

project

Management platform for a graduation events company

System that replaced spreadsheets, WhatsApp, Google Drive and paper in the operations of a graduation events company with over fifteen years in business.

Next.jsNestJSPrismaMySQLTailwind CSSPagar.meJWTHTTP-only cookies

context

Charme Eventos is a graduation events company with over fifteen years of operation. When we first started talking, the entire operation was running across four tools at the same time: spreadsheets, paper folders, Google Drive and WhatsApp. None of those tools had been chosen deliberately, they were adopted over the years as needs came up.

When I started mapping the operation, the problems had specific addresses: around R$ 2,500 a year in duplicate financial entries because receipts arrived via WhatsApp without traceability, a five-day accounting close cycle because of batch entry, and an average of five minutes to find a basic piece of data like a participant's payment status.

The first question was whether an off-the-shelf ERP could work. The problem is that graduation events have specifics that generic tools do not cover well. Each event has dozens of individual payers with different installment plans, contracts and payment methods. Forcing that into a packaged system meant training the team to work the way the software expected, instead of the other way around. I had seen what happens when a small team tries to adapt to a system built for something else. We built from scratch.

what was built

Events

  • Registration and management of graduation events
  • Individual event page with operational data and checklist
  • General information, dates and event status

Participants

  • Graduation student registration per event, with groups and classes
  • Expected amounts per participant
  • Payment tracking linked to participant and event

Finance

  • Income and expense records per event
  • PDF report export
  • Mandatory receipt attachment per financial entry
  • Integration with payments confirmed by webhook

Storage

  • Folders and files per event, modeled as a self-referential tree in the database
  • Metadata in MySQL, files on disk
  • Thumbnails generated in the background after the upload returns
  • Public links with 192-bit tokens generated with randomBytes(24)
  • Non-destructive revocation with revokedAt
  • Folder sharing with external users, with configurable permissions and expiration

Access control

  • Six modules: finance, records, attachments, charges, events and users
  • Three levels per module: none, view and manage
  • Backend validation before any read or write operation
  • Interface hides sections the user cannot access, but the backend does not rely on that

Payments

  • Public payment link with its own token
  • Frontend tokenizes the card directly in the Pagar.me API before any request reaches the backend
  • Backend receives only the card_token, never raw card data
  • Financial transaction created only after webhook confirmation
  • Unique database constraint on (sourceType, sourceId) against duplicates
  • Idempotent webhook handler with status check before writing

Audit logs

  • Records module, action, entity type and ID
  • actorType distinguishes authenticated user, public link and system
  • beforeData and afterData in JSON for sensitive changes

technical decisions

JWT authentication with HTTP-only cookie

The token is stored in a cookie with httpOnly: true and sameSite: none. It never touches the frontend JavaScript. The NestJS JWT strategy reads the cookie directly from the request.

Per-module access control with three levels

Each user has a permission record with six modules: finance, records, attachments, charges, events and users. Each module accepts none, view or manage. The interface hides what the user cannot access, but the backend validates before any operation. Hiding on screen alone is not enough.

File storage as a self-referential tree

The team was used to Google Drive, so the interface was designed to look like it: thumbnails, drag selection, moving between folders. The learning curve was almost zero. In the database, folders and files share one table with a parentId. The database record is written first. If the disk fails, the record is deleted in the catch block. That way the database never has an orphaned reference.

Public links with 192-bit tokens

Tokens are generated with randomBytes(24), not sequential IDs. Revoking sets revokedAt but does not delete the record. The history of who generated it, when, and when it was revoked is preserved. When someone navigates inside a shared folder, the system traverses the tree to confirm the destination is still within the link scope. Not trusting only the request parameters is what prevents someone with a folder link from accessing a sibling folder by manipulating the parentId in the URL.

Card tokenization on the frontend

The frontend sends card data directly to the Pagar.me API and receives a card_token. The backend only receives this token, never the raw data. The financial transaction is created only inside the webhook handler, after the gateway confirms payment. The charge only becomes a financial entry after actual confirmation from the gateway.

Idempotent webhook with database constraint

The handler checks the current status before writing anything. If the charge is already marked as paid, it returns without creating anything. Pagar.me can resend the same webhook more than once due to connection loss or automatic retry. Without this check, the system would create two financial entries for a single payment. The database also has a unique constraint on (sourceType, sourceId) as a second line of defense.

SHA-256 hash on financial receipts

Duplicate entries were one of the most expensive problems and also one of the hardest to audit. When receipts were printed and entered manually, there was no way to know afterward whether that same receipt had already been entered. The solution was to compute a SHA-256 hash of the file buffer on upload and compare it against existing receipts. If it matches, the file is rejected before reaching the database or disk. It is instant and eliminates the most common duplication case without relying on the user paying attention.

screenshots

Storage module interface with folder structure and thumbnails

Storage module with folder structure and event photo thumbnails

Folder sharing modal with permission settings

Sharing modal with configurable expiration, download and subfolder options

System blocking duplicate receipt

System rejecting a receipt already linked to another financial entry

Charges list with payment link

Charges list with real-time status and copyable link for sending via WhatsApp

what I learned

The most important thing I learned in this project is that digitalizing a small business has less to do with code and more to do with adoption. When the system forced the user to step out and use another tool, they went back to the spreadsheet.

Most of the value I delivered was not in the most technically interesting parts. It was in interface decisions: where to place a button, which field to make required, how to group the list of financial entries. Things that seem small but that make someone choose the system over the old spreadsheet.

Finance requires consistency. A duplicate entry is hard to audit after the fact. The solution was not to trust the user's attention, it was to make it physically difficult for duplicates to enter.

Management platform for a graduation events company - André Luiz