Context / Problem

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.

What it does

  • Institutional content: news with categories, featured and main-article slots, an events calendar, photo galleries (with photographers and event dates), and a search across everything, backed by a generated sitemap.
  • Legislative profiles: councilmen with mandates, the board of directors (mesa diretora), former presidents, parliamentary committees, and parliamentary fronts — public pages for each, fed by an internal legislature module.
  • Transparency portal: structured pages and tables under the council’s transparency section, with one-click export in seven formats — CSV, DOCX, Excel, HTML, JSON, PDF, and TXT — and per-page download toggles.
  • Identity card (RG) scheduling: a full appointment system where citizens pick a free slot for their ID card, and staff approve or reject from a dashboard — with working-hours configuration, holiday and block management, rescheduling, and a status flow (available → unavailable → confirmed → finished, with blocked as a dead end).
  • Legislative school: events, and a certificate module that generates PDFs, imports certificates in bulk from CSV, and lets citizens verify a certificate publicly.
  • Static and legal pages: contact, the Women’s Advocacy Office (Procuradoria da Mulher), accessibility statement, public access, city history, org chart, satisfaction survey, normative acts, FAQ, service charter, privacy policy, and terms of use.
  • Social presence: Instagram (post fetching, webhooks for verification/deauthorization/LGPD data-deletion, token refresh) and YouTube (session videos fetched on a schedule), both cached.

Constraints

  • Legal compliance is a feature, not an aspiration. The portal must satisfy the Access to Information Law (LAI), the transparency criteria of the courts of accounts, LGPD (Brazil’s GDPR) for the citizen data it collects in appointments, and accessibility expectations for a public site serving every demographic.
  • The institutional context. Legislative data has a source of truth — SAPL, the official Brazilian legislative support system, hosted at sapl.parnamirim.rn.leg.br. The portal had to integrate with it without becoming dependent on it.
  • Legacy reality. Councilmen existed as a flat table; the board of directors was a string column — the 2022 board was overwritten the moment 2023 was typed in. There was no historical record anywhere.
  • Shared hosting. Production runs on classic shared hosting (htdocs, a php8.4 binary, MySQL/MariaDB) — no containers, no serverless, no autoscaling. Everything must fit one server and one deployable.
  • A small team, a big surface. The feature set above shipped iteratively, module by module, with quality gates that could not slow the team down.

Architecture Decisions

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.

Challenges

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 , 1o, , 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.

Trade-offs

  • Mirror, not proxy: data is up to a day stale (the daily sync) — in exchange, public pages never fail because SAPL is down, never slow down from remote API calls, and always render manual + synced data together.
  • A migration, not a rewrite: the dashboard is moving from Livewire to Vue 3 + Inertia module by module, so the codebase temporarily holds two frontend paradigms. That’s the cost of the transition — paid in exchange for a reactive dashboard on the stack I use everywhere else, with the public site untouched.
  • One deployable monolith: the whole portal ships in one release, which is right for a small team, and the modular structure keeps the escape hatch honest.
  • Shared hosting over managed cloud: near-zero cost and zero infrastructure novelty, paid for with careful deploy scripts and scheduled queue windows instead of autoscaling — with redundancy built into the setup, the portal has held 100% uptime over the last two years.
  • Human-gated deploys: safer for a public institution, slower for iteration — the right trade when a bad release means citizens can’t schedule their ID cards.

Result

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.