A city council is a small government with a large public-facing surface. The Câmara Municipal de Parnamirim — a municipality of ~270k people in Rio Grande do Norte, Brazil — needed to talk to its citizens daily: publish news, announce events, show who the councilmen are, answer questions, and above all comply with Brazilian transparency law. The previous portal was a static shell that did none of this well, and the legislative information that mattered most — who sits on the board, which committees exist, what each legislature did — was either hand-maintained text or lived in an external system the citizens had to leave the site to use.
The project became the council’s digital front door: a platform that publishes institutional content, delivers citizen services, and demonstrates transparency at the highest national standard. That last point is measurable: the portal helped the council win the Selo Diamante de Transparência Pública — the highest tier of the federal Programa Nacional de Transparência Pública, granted by ATRICON (the association of Brazilian courts of accounts) through TCE-RN. Parnamirim is now a four-time champion of transparency in Rio Grande do Norte — most recently 2025, with a 95.17% score and the only institution in the municipality holding the seal — and sits in a national elite group: of the 8,019 entities evaluated across Brazil, only 370 reach the Diamond tier.
available → unavailable → confirmed → finished, with blocked as a dead end).sapl.parnamirim.rn.leg.br. The portal had to integrate with it without becoming dependent on it.htdocs, a php8.4 binary, MySQL/MariaDB) — no containers, no serverless, no autoscaling. Everything must fit one server and one deployable.Laravel 12 + PHP 8.4. Boring in the best way: authentication (Fortify), API tokens (Sanctum), queues, scheduling, validation, and migrations are all batteries included and well documented. For a government contract, predictability and long-term maintainability beat novelty.
Vue 3 + Inertia dashboard, migrating away from Livewire. The admin dashboard is being rebuilt as an Inertia + Vue 3 SPA — the legislature module was born directly on it, and users, permissions, activity log, councilmen, committees, and parliamentary fronts have already moved. Inertia is the pragmatic middle ground: routes, controllers, validation, and authorization stay pure Laravel (Ziggy exposes named routes to JS), while the frontend gets a real reactive component model with shared layouts and UI primitives. The public site remains server-rendered Blade + Bootstrap — fast, accessible, and easy for any PHP developer to pick up — and the older modules still on Livewire (the RG scheduler, the certificate manager, the transparency page builder) are on a documented path to the new stack. The migration exists because the heaviest interactive surfaces kept pushing Livewire’s limits, and because Vue 3 is the frontend I reach for everywhere else. Vite and Bun handle the asset build; imask masks CPF/phone inputs and html2canvas renders appointment receipts.
A modular monolith with a planned escape hatch. Each feature is a module with its own controllers, models, views, and request classes: IdentityScheduler, LegislativeSchool, Legislature, Transparency. Views live under dashboard/{module}/, controllers under App\Http\Controllers\{Module}\. The README documents the migration path to fully independent module folders once the module count makes a monolith uncomfortable — deploy independence without paying for microservices today.
The SAPL mirror: sync, don’t proxy. This is the decision I’m proudest of. SAPL is the official system of record for legislative data, but calling it from public page requests would make the portal hostage to an external API’s latency and uptime. Instead, a scheduled command (sapl:sync, daily at 04:00) mirrors SAPL into local tables, and public pages read only Eloquent — no HTTP in the request path. The mirror is structural: synced rows carry a sapl_id and source = 'sapl', manual rows have sapl_id = null, and every sync write is updateOrCreate(['sapl_id' => ...]) — which is incapable of touching a manual record. No destructive deletes; removed entities are deactivated or dated, because in a legislature, history is the point. Thirteen resources sync in dependency order (legislatures → parties → councilmen → mandates → sessions → boards → committees → fronts, with their members), each isolated in its own try/catch so one broken endpoint degrades to partial instead of aborting the night’s sync.
The legislature module as the historical axis. The flat councilmen table grew into a real domain: legislatures (with date ranges, not years — years are a form concern), mandates (a model that is also a pivot, since a councilman can be titular and then substitute in the same legislature), legislative_sessions, boards and board_members (with a normalized BoardPositionEnum that swallows the legacy °/º inconsistencies), committees, parliamentary_fronts, and political_parties (so a mandate records the party at the time, not the councilman’s current one). The legacy string board of directors was backfilled into proper rows by an idempotent artisan command + migration, and the string column was dropped afterwards with a rollback that rebuilds it.
Transparency as a product. The transparency portal is not a PDF dump: it’s structured categories and pages, with content timestamps and an export engine that renders the same data to seven formats. This is the feature the Selo Diamante evaluators actually click through — data accessibility, clarity, and ease of navigation are exactly the scored criteria.
The RG scheduler. A queued citizen service with real state: configurable working hours, capacity, blocks, and holidays; slot generation (manual and batch); auto or manual approval; rescheduling; CPF and birth-date validation; and an X-Edit-Key-free design — citizens authenticate by their appointment details instead of creating accounts, keeping LGPD surface small.
Media pipeline. spatie/media-library with a custom path generator, a dedicated media queue for heavy processing (resizes, optimization) scheduled for the early morning, and a default queue for the rest. Galleries track photographers and event dates — details the council’s communication team actually needs.
Observability and quality gates. PostHog for product analytics, Sentry for errors, and a spatie activity log (with a scheduled cleanup) for auditability — the last one is practically a legal requirement in this domain. The engineering bar is enforced in CI: Pint formatting, Larastan (PHPStan) at level 3, and a Pest suite of 3,000+ tests running on SQLite in-memory with DatabaseMigrations — which forced the codebase to drop MySQL-only SQL like FIELD() and keep queries portable.
Deploy with a human in the loop. GitHub Actions runs Larastan + tests + both builds on every push. Production deploy triggers on a version tag, or manually — and the manual trigger requires typing yes as confirmation. The deploy script is a careful dance for shared hosting: artisan down, composer install with --no-dev, migrations with --force, config/route/view caching, artisan up. A parallel workflow serves the stage site at develop.camaraparnamirim.rn.gov.br — government work gets a staging environment as a matter of course.
Living with two frontends. Mid-migration, the codebase deliberately holds two paradigms: Blade/Livewire for the older modules and Inertia/Vue for the new ones. The rules are strict — new work ships on the new stack, and migrations happen module by module with feature parity — but coexistence means the same patterns (layouts, validation messages, authorization checks) exist in both worlds until the old one is retired. The dashboard root view, shared props, and page scaffolding had to be stood up while a production government site kept receiving feature requests.
The SAPL API is not friendly. Its pagination wrapper is not standard DRF (pagination.links.next + total_pages), ativo=true returns HTTP 400 (it only accepts True/False), foreign keys come as plain ints unless you pass ?expand, and the real data contains orphaned board rows and legacy mesa_diretora = null entries that must be skipped and logged, not crashed on. Every quirk became a helper with its own test (boolParam, fkId, a paginate() generator that follows next with a total_pages guard against self-referential loops).
Preserving the manual records. The council’s team edits councilmen by hand — those edits must survive daily syncs. The sapl_id invariant makes it structural, but the trickier part is identity: a manual councilman and a SAPL councilman for the same person have no automatic match. The answer was a --dry-run --match-by=email report plus an explicit “Link to SAPL” action in the dashboard, done before the sync is enabled — never an automatic guess.
Photo protection. SAPL provides photos, but the communication team crops and retouches them by hand. The sync downloads SAPL photos only when a councilman has no media, or when the existing media is itself SAPL-sourced and the URL changed. Hand-made edits are never silently overwritten.
History without a time machine. Backfilling the board of directors meant creating a deterministic placeholder legislature when none exists, mapping legacy position strings through a normalizer that collapses 1ª, 1o, 1º, 1° and primeiro into one canonical value, and making every rerun idempotent. Nothing was thrown away: unmapped values became Membro with a warning log.
LGPD discipline. Appointment data is personal data, so the surface is minimized (no accounts, appointment-detail-based access), Instagram’s data-deletion webhook is implemented, and the SAPL token lives only in .env — deliberately not in PortalOption, because the activity log’s logAll() would have copied it in plaintext into the audit trail.
Shared-hosting deploy choreography. No sudo, no docker, a specific php8.4 binary path, and a deploy that must be reversible. The artisan down/up pair, cache steps, and the human yes gate turned a fragile SSH ritual into a repeatable, approved procedure.
The portal is live at camaraparnamirim.rn.gov.br and is the council’s daily communication and service channel. It holds the Selo Diamante de Transparência Pública from ATRICON/TCE-RN — a federal award the council has now won four times, making it the transparency champion of Rio Grande do Norte (most recently 2025, with a 95.17% score) and part of a national elite group of institutions at the Diamond level — the only one in the municipality. Citizens schedule ID-card appointments, look up legislative-school certificates, browse every committee and front, and export transparency data in the format they need. The production setup — with redundancy in place — has held 100% uptime over the last two years. The work is tracked in Notion with [PCM-XXX] tickets, and every release is verified by Larastan and a suite of 3,000+ tests before a human types yes.