TL;DR. We built a custom case-management system for Lothus Visas, a Brazilian immigration firm, to replace a spreadsheet. Four decisions carried the project: case status as a state machine, deadlines as first-class data, an append-only audit trail, and a read-only client portal. Here is why each mattered, and what we would do differently.
The real competitor was a spreadsheet
When we started the Lothus Visas project, the firm was not using a rival SaaS. They were using one spreadsheet. One tab per corporate client, color codes only one person fully understood, and expiry dates for visas and residence permits tracked by memory and discipline.
That is the honest starting point for most build-vs-buy conversations. The competitor is rarely Docketwise or Clio. It is the spreadsheet that grew for five years and now holds the whole operation hostage.
Spreadsheets fail in predictable ways for immigration work:
- No enforced status. Anyone can type anything in the status column.
- No alerts. A date in a cell does not warn anyone. Someone has to look.
- No history. When a field changes, the old value is gone and nobody knows who changed it.
- No client visibility. Every "how is my visa going?" question becomes an email, a lookup and a reply.
The system we built (internally, lv-system) answers each of those failures with one deliberate decision. Stack: Next.js, Prisma, Postgres, and next-intl for a multilingual interface, since the firm serves clients in more than one language. Nothing exotic. The interesting part is the modeling, not the framework.
Decision 1: status is a state machine, not a text field
The first schema discussion was also the most important one. A visa case has a lifecycle: documents being gathered, filed, under review, approved, and so on. The lazy version is a status string column. The version that survives contact with reality is an enum plus transition rules.
Why does this matter so much?
- Enums keep the data clean. With string-backed enums, the values in Postgres stay readable by a human running a query, while the application code stays strictly typed. "Approved" cannot coexist with "aproved".
- Valid transitions protect integrity. A case cannot jump from "documents pending" to "approved" by accident. The application checks whether a transition is allowed before writing it. This is the standard guidance in workflow modeling references like the commercetools state-machine docs: enforce transitions, or your states are decorative.
- Reports become trustworthy. "How many cases are under review per client?" is a one-line query when states are enums. It is an archaeology project when states are free text.
One practical benchmark we followed: if a workflow grows past roughly seven to nine states, split it into sub-machines instead of one giant list. A visa case and a residence-permit renewal are different processes. Forcing them into one state list makes both worse. Checklists per procedure type handle the variation inside each state, so the machine itself stays small.
Decision 2: deadlines are data, not calendar entries
Here is the part that makes Brazilian immigration different from the US-centric SaaS market. The core recurring deadline is not a USCIS form window. It is the RNM, the Registro Nacional Migratório, the residence registration that foreign employees must renew on time. Miss it and the problem lands on the client's employee, then on the client, then on the firm.
The old approach was a date in a spreadsheet cell plus human vigilance. The system's approach: every visa and every RNM has its expiry date as a real field on the record, and a scheduled job derives alerts from those dates. Nobody creates a reminder. The reminder exists because the data exists.
This sounds like a convenience feature. It is closer to risk management. The ABA's Profile of Legal Malpractice Claims consistently lists calendaring and missed-deadline errors among the top causes of malpractice claims, and it flags date-calculation mistakes as a recurring error even among experienced lawyers. Legal-tech vendors have long credited calendaring software with reducing deadline-related claims. The lesson generalizes: a deadline that only exists in someone's head, or in a cell nobody is watching, is a liability on the balance sheet.
Design detail worth stealing: track the underlying real-world date, not just a task due date. "RNM expires on this date" is a fact about the world. "Renew RNM by this date" is a task derived from that fact, with a safety margin. Store the fact, derive the task. When the rules change, you change the derivation, not a thousand hand-entered reminders.
Decision 3: an audit trail that answers "who changed what"
In a firm where several people touch the same case, "who changed this status and when?" is a question you will get asked. Usually on a bad day. The audit trail is the difference between an answer and a shrug.
We followed the OWASP Logging Cheat Sheet as the baseline for what an entry must record:
- When: timestamp.
- Who: the authenticated user, not a shared login.
- What: event type, the action taken, and the affected record.
- Result: did it succeed or fail.
Beyond field changes, the same guidance says to always log authentication events, authorization failures, and privilege or admin changes. And two properties are non-negotiable: the trail is append-only, so entries are never edited or deleted, and audit logging can never be fully disabled. An audit trail someone can quietly turn off is theater.
In Prisma terms this is unglamorous: an AuditLog model with user, timestamp, entity, action and a before/after snapshot, written in the same transaction as the change itself. Same transaction matters. A log written "later, probably" is a log with holes.
There is a market reason to do this properly too. Compliance regimes like SOC 2, and in Europe DORA and NIS2, are pushing tamper-evident logs down into small B2B software. Building it in from day one costs a few models and a habit. Retrofitting it costs a migration and an apology.
Decision 4: a read-only client portal, on purpose
The feature the firm's clients feel most is the smallest one technically: each corporate client gets a portal where they can see the status of their employees' cases. Read-only. No uploads, no chat, no comments.
That scoping was deliberate, and it is worth defending, because the 2026 checklist for immigration portals is much longer: real-time milestone updates, secure document upload, two-way messaging, multilingual support. We shipped the status-visibility slice and skipped the rest.
Why status first? Because that is where the pain concentrates. A study cited by Law Technology Today, referenced in CampLegal's 2026 immigration-software comparison, found that 72% of clients prefer law firms offering real-time case updates through online portals. The email loop the firm wanted to kill was precisely "how is my visa going?". A read-only status page answers that question at 11 pm on a Sunday without anyone writing an email.
Why not uploads and messaging in version one? Each of those brings real weight: file storage and virus scanning, retention rules, response-time expectations, notification fatigue. Bolting them onto v1 would have delayed the thing that mattered. Scoping per corporate client, so HR at company A sees only company A's people, was already enough access-control work for a first release.
Build vs. buy, and what we would change
Would an off-the-shelf platform have been cheaper? The honest comparison:
Where the incumbents win. Docketwise, INSZoom, LollyLaw and Clio are built around one superpower: auto-populating 150 or more US government forms (USCIS, DOL, the DS series). If your practice is US filings, that automation is the product, and the 2026 wave of AI features (auto-filling forms, flagging outdated form versions) makes it stronger. Mid-tier platforms run roughly USD 69 to 99 per user per month, which is not crazy for what they do.
Where they lose for this firm. None of them speak Brazilian immigration. No RNM concept, no local filing formats, no export in the shape the firm actually files. The firm would pay per user per month and still keep the spreadsheet for everything Brazilian, which is to say, for the core business. Buyer behavior in this market splits by segment: solo firms want out-of-the-box platforms, corporate immigration practices want compliance tracking and per-company client portals. Lothus sits squarely in the second pattern, and that pattern is exactly what we built: portal per corporate client, RNM alerts, exports in the firm's own reporting format. Reporting, by the way, is still an underrated selling point: industry surveys suggest fewer than 40% of law firms use analytics tools at all, so a clean Excel export in the right shape beats a dashboard nobody opens.
What we would keep. The state machine and the audit trail, without hesitation. They are the two decisions that cost little upfront and compound forever. Deadlines as first-class data, same.
What we would change. Two candidates. First, we would build the notification infrastructure earlier. Alerts started life inside the app and grew channels later; treating notifications as their own module from week one would have been cleaner. Second, document upload in the portal is the obvious next slice, now that read-only status has proven itself. We would plan its permission model in v1 even while shipping it in v2. We will not quote success percentages we never measured. The qualitative outcome, though, was the one the firm asked for: fewer status emails, and no more operation that lives inside one person's spreadsheet.
Thinking about replacing your own spreadsheet?
If your operation runs on a spreadsheet only one person understands, the fix is rarely "more discipline". It is a small system with the right four bones: enforced states, deadlines as data, an audit trail, and just enough client visibility. That is the kind of internal system we build at Impulse, and the first conversation is about your workflow, not about frameworks. If this story sounds like your Monday, talk to us.



