Decision Records
Every significant architectural decision in SaaS4Builders is documented in a decision log. This practice — inspired by Architecture Decision Records (ADRs) — gives you and your team a clear history of why things are built the way they are.
Where Decisions Live
The decision log lives in the repository root alongside the codebase. Each entry follows a simple four-field template:
## YYYY-MM-DD: Decision Title
**Context:** Why this decision was needed.
**Decision:** What was decided.
**Alternatives considered:** What else was evaluated.
**Consequences:** Impact on the codebase.
Key Decisions Shipped with the Boilerplate
The following architectural decisions are documented in the decision log. They reflect the foundational choices that shape the entire codebase.
Multi-Tenancy: Single Database with tenant_id Scoping
Decision: Single database with tenant_id column scoping via a BelongsToTenant trait and global Eloquent scope.
Why not multi-database? Too complex for V1 — harder to query across tenants for admin dashboards, migration complexity multiplied by tenant count.
Why not schema-based? PostgreSQL-specific, migration complexity, harder to reason about.
Impact: All tenant-scoped models must use the BelongsToTenant trait. Admin queries explicitly bypass the global scope. See Multi-Tenancy for details.
Tenant Resolution: From Authenticated User
Decision: Resolve tenant from the authenticated user's tenant_id column.
Why not subdomain-based? More complex DNS and SSL setup for V1.
Why not path-based? Clutters URLs with /tenants/acme/... prefixes.
Impact: Public routes do not have tenant context. The frontend supports multiple tenancy modes (header, path, subdomain) for future flexibility — see Vertical Slices.
Billing: Stripe as Payment Processor
Decision: Stripe handles payment processing. The SaaS application is the seller of record with internal invoice generation.
Why not LemonSqueezy/Paddle? Initially considered. Simpler (Merchant of Record), but less control over billing flows, invoicing, and multi-currency.
Impact: Stripe handles checkout, subscriptions, webhooks, and optional tax calculation. Domain Contracts abstract Stripe, making future provider additions possible. See Infrastructure Layer.
API Versioning: URL-Based (/api/v1/)
Decision: URL-based versioning with /api/v1/ prefix.
Why not header-based? Harder to test and debug (Accept: application/vnd.api.v1+json).
Impact: Controllers are namespaced under App\Http\Controllers\Api\V1\. Breaking changes require a new version. See API Contracts.
Authentication: Laravel Sanctum
Decision: Sanctum with cookie-based auth for SPA, token-based for API clients.
Why not Passport (OAuth2)? Overkill for a first-party SPA.
Why not JWT? Stateless but harder to revoke, token refresh complexity.
Impact: The SPA uses cookies with CSRF protection. Mobile or third-party clients use Bearer tokens. See Authentication.
Frontend State: Pinia + Composables
Decision: Pinia stores for shared global state, composables for feature-specific reactive logic.
Why not Vuex? Legacy — Pinia is the official successor.
Why not composables only? Harder to share state across unrelated components without a store.
Impact: Stores manage raw data and loading state. Composables wrap stores as facades, adding computed helpers and permission checks. See Composables & Stores.
Single Tenant Per User
Decision: Each user belongs to exactly one tenant. Platform admins have no tenant.
Why not multi-tenant per user? Adds complexity to tenant resolution, context switching UI, and permission scoping — not needed for V1.
Impact: User has a nullable tenant_id column. No tenant-switching UI. Simplified auth and tenant middleware.
Testing: PHPUnit + Vitest
Decision: PHPUnit for backend, Vitest for frontend.
Why not PestPHP? Initially used, migrated for Laravel Boost compatibility.
Why not Jest? Slower than Vitest for Vue/Nuxt projects.
Impact: Backend tests in tests/Feature/ and tests/Unit/. Frontend tests co-located with features. See Testing.
Real-Time: Laravel Reverb (WebSocket)
Decision: Laravel Reverb for WebSocket server, Laravel Echo for the client.
Why not polling? Higher latency, more server load.
Why not Pusher? External dependency and cost.
Impact: Reverb runs as a dedicated Docker container. Tenant-scoped private channels (private-tenant.{tenantId}). Zod validation on incoming WebSocket payloads.
Pending Decisions
These decisions are documented as open in the decision log:
- Caching strategy — Redis structure and cache invalidation patterns
- Queue driver — Redis vs SQS vs database for production
- File storage provider — Local vs S3 vs R2
Recording Your Own Decisions
When you make a significant architectural decision for your product, add it to the decision log. A good rule of thumb: if someone might ask "why did we do it this way?" six months from now, document it.
Template
## 2026-03-26: Your Decision Title
**Context:** The problem or requirement that prompted this decision.
**Decision:** What you decided to do.
**Alternatives considered:**
- Option A: Why it was rejected.
- Option B: Why it was rejected.
**Consequences:**
- Impact on the codebase.
- What must be true going forward.
When to Write a Decision Record
| Write one when... | Skip when... |
|---|---|
| Choosing between architectural approaches | Following an established convention |
| Picking a new library or service | Updating a dependency version |
| Changing a convention or pattern | Fixing a bug |
| Making a trade-off with long-term impact | Refactoring without behavioral change |
Guidelines
- Lead with context. The reader needs to understand the problem before the solution makes sense.
- List alternatives honestly. Future-you or a teammate might revisit this decision.
- State consequences explicitly. What constraints does this create? What must other developers follow?
- Date the entry. Use the ISO date format (YYYY-MM-DD) so decisions have a timeline.
- Keep it concise. One page per decision. If you need more, the decision might be too broad.
Real-World Example
Here is a filled-out decision record from the boilerplate, illustrating the format:
## 2026-XX-XX: Billing Provider
**Context:** Need payment processing with subscription support.
The boilerplate must handle flat-rate, seat-based, and usage-based pricing
with multi-currency support and tax compliance.
**Decision:** Stripe as payment processor. SaaS remains seller of record
with internal invoice generation.
**Alternatives considered:**
- LemonSqueezy: Simpler (Merchant of Record), but less control over billing.
- Paddle: Similar to LemonSqueezy, less developer-friendly API.
**Consequences:**
- Stripe handles: Checkout, Subscriptions, Webhooks, Tax calculation.
- Internal invoice generation (not Stripe-managed invoices).
- Webhooks are source of truth for subscription lifecycle.
- Domain Contracts abstract Stripe — future providers possible.
Notice:
- The context explains why a decision was needed (not just "we needed a billing provider")
- Alternatives include the reason they were rejected (not just a list of names)
- Consequences state what the decision forces on the rest of the codebase
How Decisions Connect to the Codebase
Decision records are not isolated documents. They connect to concrete code patterns:
| Decision | Codebase Impact |
|---|---|
| Single DB tenancy | BelongsToTenant trait, global scopes, tenant isolation tests |
| Stripe as provider | PaymentGatewayInterface, StripePaymentGateway, BillingServiceProvider bindings |
| URL-based versioning | App\Http\Controllers\Api\V1\ namespace, /api/v1/ route prefix |
| Sanctum auth | Cookie-based SPA auth, useApiClient() CSRF handling |
| Pinia + composables | Store → composable facade pattern, useAuthenticatedAsyncData() |
| Single tenant per user | Nullable tenant_id on User, no tenant-switching UI |
When you read a decision record, follow the "Consequences" section to understand where that decision manifests in the code. When you encounter an unfamiliar pattern in the code, check the decision log for the rationale.
What's Next
- Architecture Overview — The mental model for the entire codebase
- API Contracts — One of the key decisions (snake_case convention) in action
- Infrastructure Layer — How the Stripe decision shapes the billing infrastructure
API Contracts
The contract system bridging Laravel Resources (backend) and Zod schemas (frontend): casing, money, dates, pagination, errors, and the full endpoint flow.
Auth Architecture Overview
How SaaS4Builders handles authentication with Sanctum dual-mode, token pairs, middleware, and role-based permissions.