← Back to Project|Technical Documentation
Public · Read-only

Architecture tradeoffs and the rationale behind them

Key Design Decisions

These decisions represent the major architectural and structural choices made during the 6-week development period. Each was made in response to a real constraint, bug, or performance issue discovered during active development.


1 — Unified Transaction Table vs. Split Tables

Context (Week 2): The initial design considered separate transaction tables per document type (e.g., clearance_transactions, indigency_transactions).

Decision: Adopted a single document_transactions table with:

  • document_type_iddocument_type_properties for type resolution
  • doc_type_model field for polymorphic routing to type-specific detail tables

Why it matters:

  • A single table means a single query to fetch all pending requests in a barangay's queue
  • One authorization middleware covers all document types
  • Audit trail is unified — every transaction lifecycle is in one place
  • New document types require only a new detail table + a row in document_type_properties, not schema changes to the core transaction table

2 — Term-Based Approver ID

Context: The original schema stored user_id as the approver on document_transactions.

Decision: Changed to barangay_terms.id instead of users.id.

Why it matters: Storing a term_id permanently links each document to the specific legal capacity in which the official was authorized to sign at that moment. Years after a Captain leaves office, the system can still answer:

"Who was legally authorized to sign this document, in what position, and was that position still active at the time of signing?"

This is critical for provincial governance accountability and satisfies audit requirements that a user_id alone could not.


3 — Migration from barangay_code (String) to barangay_id (Integer FK)

Context (Feb 25–26): The initial schema used eGovPH string codes (barangay_code) as the linking identifier across tables.

Problem discovered: PSGC and eGovPH use different zero-padding formats for barangay codes (e.g., 030201 vs 30201). This format mismatch caused intermittent authorization failures where a resident's barangay couldn't be matched to an official's scope.

Decision: Full migration to integer barangay_id foreign keys across User, House, Household, Family, and BarangayTerm models.

Outcome: The barangay_code string is retained only in the barangays table itself for external API integration. All internal joins and authorization checks use the integer FK — faster, unambiguous, and safe from format drift.


4 — Spatie Laravel Permission for RBAC

Context (Feb 13): The initial implementation used a custom permission system.

Decision: Replaced with Spatie Laravel Permission.

Why: Spatie provides built-in Filament integration, model-level permission scoping, a testable permission cache, and well-understood behavior around guard scoping. The custom system had edge cases in permission inheritance that were producing inconsistent access control behavior. Moving to Spatie also reduced the maintenance surface area significantly.


5 — Vue.js → Laravel Livewire

Context (Feb 6): Initial frontend was scaffolded with Vue.js.

Decision: Migrated to Laravel Livewire.

Why: The resident portal is backend-heavy. Most interactions require real-time server validation — barangay scoping on document requests, residency eligibility checks, duplicate detection. Livewire keeps all of this logic on the server, eliminating:

  • A separate JSON API layer
  • Authorization bypass surface area from client-side enforcement
  • State sync complexity between Vue and Laravel

It also kept the stack consistent with the Filament admin panels, which already run on the server-rendered Livewire paradigm.


6 — Queue Jobs for PDF Generation and Notifications

Context (March 9): Document approval previously executed synchronously in the HTTP request cycle.

Problem: Browsershot PDF generation via headless Chromium takes 2–10 seconds depending on document complexity. This caused HTTP timeouts and blocked the official's interface during approval.

Decision: Moved to two dedicated queue jobs:

  • ProcessDocumentRequest — notifications and broadcasting, dispatched ->afterCommit()
  • ProcessDocumentApproval — PDF generation, storage, checksum, status update

The afterCommit() dispatch on ProcessDocumentRequest is critical — it ensures the DB transaction is fully committed before the job fires, preventing race conditions where an official receives a notification for a request that isn't yet readable from the database.

Configuration: 120-second timeout, 3 retries with 10-second backoff. On final failure, the transaction status reverts to pending so the official can retry manually rather than being left with a stuck "processing" state.


7 — No FK Constraint on household_head_id

Context (Feb 26): The households.household_head_id field references household_member_profiles.id.

Decision: No database-level foreign key constraint on this field.

Why: A proper FK here would create a circular dependency:

  • householdshousehold_member_profiles (via household_head_id)
  • household_member_profileshouseholds (via household_id)

Enforcing both at the DB level causes issues with insert ordering and cascade behavior. Head assignment and validation are handled at the application layer, where the full state of both tables is available.


8 — NFC Simplified to Local DB Only

Context (March 4): The NFC walk-in flow initially performed external Bataan Portal API lookups when a card was tapped.

Problem: The external API had inconsistent response times and format mismatches, causing failures at the desk during live walk-in processing.

Decision: NFC now resolves UIDs against the local database only. If a resident isn't found, staff are directed to register them first.

Why: Walk-in processing must be reliable and low-latency. A desk interaction that hangs on an external API call is unacceptable in a live service environment. The local DB always has the data for residents who have previously interacted with the system, which covers the vast majority of walk-in cases.


9 — SHA-256 Checksum + One-Time Download Token

Every issued document gets:

  • SHA-256 checksum computed from the generated PDF. Stored in document_transactions.checksum. On QR scan, the system re-computes the hash and compares. Mismatch → document flagged as Invalid/Tampered.
  • One-time download token in download_token. Refreshed on every download attempt, preventing unauthorized URL sharing.

Summary Table

DecisionTriggerOutcome
Unified transaction tableComplexity of split tablesSingle queue, single middleware, easy audit
Term-based approver IDAudit trail accuracyLegally defensible signing records
Integer barangay_id FKeGovPH code format mismatch bugsReliable joins, no format ambiguity
Spatie RBACCustom system edge casesIndustry-standard, Filament-integrated
Livewire over Vue.jsBackend-heavy validation needsSimpler stack, smaller attack surface
Queue jobs for PDFHTTP timeouts on BrowsershotNon-blocking approvals, retriable failures
No FK on household_head_idCircular dependencyApplication-layer enforcement instead
NFC local DB onlyExternal API unreliabilityReliable desk operations
Checksum + one-time tokenDocument forgery / sharingTamper detection + access control