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_id→document_type_propertiesfor type resolutiondoc_type_modelfield 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:
households→household_member_profiles(viahousehold_head_id)household_member_profiles→households(viahousehold_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
| Decision | Trigger | Outcome |
|---|---|---|
| Unified transaction table | Complexity of split tables | Single queue, single middleware, easy audit |
| Term-based approver ID | Audit trail accuracy | Legally defensible signing records |
Integer barangay_id FK | eGovPH code format mismatch bugs | Reliable joins, no format ambiguity |
| Spatie RBAC | Custom system edge cases | Industry-standard, Filament-integrated |
| Livewire over Vue.js | Backend-heavy validation needs | Simpler stack, smaller attack surface |
| Queue jobs for PDF | HTTP timeouts on Browsershot | Non-blocking approvals, retriable failures |
No FK on household_head_id | Circular dependency | Application-layer enforcement instead |
| NFC local DB only | External API unreliability | Reliable desk operations |
| Checksum + one-time token | Document forgery / sharing | Tamper detection + access control |