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.
Frequently asked questions
Should an immigration firm build custom case-management software or buy an off-the-shelf SaaS?
Buy if your work maps to US forms, since platforms like Docketwise auto-populate over 150 USCIS and DOL forms and that is hard to beat. Build if your workflow depends on local rules the big platforms ignore, like Brazilian RNM renewals and local filing formats. For a firm paying per user every month for software that still needs spreadsheets on the side, custom often wins.
What is a status workflow and why not just use a free-text status field?
A status workflow is a fixed set of states a case can be in, plus rules for which transitions are allowed. A free-text field lets anyone type anything, so "Approved", "approved" and "aproved" become three different statuses and reports fall apart. With enum-backed states in the database, a case can only move along valid paths and the data stays trustworthy.
What should an audit trail record in a B2B system?
Following the OWASP Logging Cheat Sheet, each entry should record when it happened, who did it, what they did and what the result was. That means timestamp, authenticated user, event type, the action and its outcome. Log authentication events, permission failures and sensitive-data access too, keep the trail append-only, and never let it be fully switched off.
How do automatic deadline alerts work for visa and residence permit expirations?
The expiry date lives on the record itself, as a real database field on each visa or residence permit. A scheduled job compares those dates against alert windows and notifies the responsible person before the deadline, not after. Because alerts derive from the data, a date that is in the system can no longer be silently forgotten.
What should a client portal show clients, and why make it read-only at first?
At minimum, the current status of each case, mirroring the real steps of the process, scoped so each corporate client sees only their own people. We shipped read-only on purpose because status visibility kills most "how is my case going" emails on its own. Uploads and messaging add permissions, storage and moderation concerns, so they belong in a later phase, not in version one.
What does it take to build an internal system like this with Next.js, Prisma and Postgres?
The stack itself is standard, Next.js for the app, Prisma over Postgres for the data model, next-intl for a multilingual interface. The real work is domain modeling, agreeing on the states, the deadline types and who is allowed to change what. Get that right on paper first and the code follows quickly; get it wrong and no framework saves you.
