-
-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture
Saiful Alam Rakib edited this page Apr 22, 2026
·
1 revision
Bill Organizer is a full-stack web application using an Inertia.js monolith — a single Laravel application serving a Vue.js SPA without a separate API layer for the frontend, while also exposing a versioned REST API for external clients.
| Layer | Technology |
|---|---|
| Framework | Laravel 12 (PHP 8.3+) |
| Database | SQLite (default) · MySQL 8.0+ |
| Authentication | Laravel Sanctum (API tokens) + session auth (web) |
| API | Inertia.js (SPA bridge) + REST API v1 |
| Queue System | Database-backed queue |
| Configurable mail drivers (SMTP, log, etc.) | |
| Testing | Pest PHP |
| Code Style | Laravel Pint (PSR-12) |
| Layer | Technology |
|---|---|
| Framework | Vue.js 3 with TypeScript |
| Build Tool | Vite |
| UI | Tailwind CSS v4 |
| Components | Reka UI, Lucide icons |
| Charts | ApexCharts |
| Forms | VeeValidate + Zod validation |
| State | Vue 3 Composition API |
| Layer | Technology |
|---|---|
| Containerization | Docker + Laravel Sail |
| CI/CD | GitHub Actions |
| Package Manager | Yarn (frontend) · Composer (backend) |
| Linting | ESLint, Prettier (frontend) · Pint (backend) |
The application uses a team-based multi-tenancy approach:
User ──┬──► Team A (owner)
├──► Team B (member)
└──► Team C (member)
Active Team = single context for all data operations
- Teams are the central organizational unit.
- Users can belong to multiple teams and switch between them freely.
- Active Team — each user session operates within one team at a time.
-
Data Isolation — Eloquent global scopes (
TeamScope) automatically filter all queries to the current team, preventing cross-tenant data leaks.
app/
├── Enums/ # PHP enums (BillStatus, RecurrencePeriod, etc.)
├── Helpers/ # Helper functions and utility classes
├── Http/
│ ├── Controllers/
│ │ ├── Api/V1/ # REST API controllers (AuthController, BillController, …)
│ │ └── (web) # Inertia web controllers
│ ├── Middleware/ # Custom middleware (team middleware, etc.)
│ └── Resources/
│ └── Api/V1/ # API resource transformers (BillResource, etc.)
├── Jobs/ # Queued jobs
├── Mail/ # Mailable classes
├── Models/
│ ├── Pivots/ # Custom pivot models (e.g. TeamUser)
│ └── Scopes/ # Eloquent global scopes (TeamScope)
├── Notifications/ # Laravel notification classes
├── Providers/ # Service providers
└── Traits/ # Reusable traits (HasTeam, HasMetaData)
resources/js/
├── Components/ # Shared Vue components
│ └── notes/ # Note-specific components (NoteCard, NoteViewDialog)
├── Layouts/ # Page layout wrappers
├── Pages/ # Inertia page components (Bills, Categories, Notes, …)
├── Types/ # TypeScript type definitions
├── Utils/ # Utility helpers
└── composables/ # Vue composables (useNoteExport, etc.)
routes/
├── web.php # Inertia web routes
├── api.php # API entry point (loads versioned files)
└── api/
└── v1.php # All /api/v1/* route definitions
User
├── belongsToMany(Team) → teams (via team_user pivot)
├── belongsTo(Team, active_team) → currently active team
├── hasMany(Bill)
├── hasMany(Category)
└── HasApiTokens (Sanctum)
Team
├── belongsTo(User, owner) → team owner
├── belongsToMany(User) → members
└── Global scope for data isolation
Bill
├── belongsTo(User)
├── belongsTo(Team)
├── belongsTo(Category)
├── hasMany(Transaction)
└── morphToMany(Note)
Transaction
├── belongsTo(Bill)
└── belongsTo(Team)
Note
├── belongsTo(User)
├── belongsTo(Team) → null for personal notes
└── morphedByMany(Bill, …) → polymorphic relationship
| Table | Purpose |
|---|---|
users |
User accounts, active team reference |
teams |
Team workspace definitions |
team_user |
Many-to-many users ↔ teams pivot |
bills |
Bill records with recurrence and trial support |
categories |
Bill categories (with icon and color) |
transactions |
Payment records linked to bills |
notes |
Polymorphic notes (personal or team-scoped) |
meta_data |
Flexible key-value store for preferences and flags |
personal_access_tokens |
Sanctum API tokens |
Automatically assigns the current team ID when creating a model:
trait HasTeam
{
protected static function bootHasTeam()
{
static::creating(function ($model) {
$model->team_id = Team::current()->id;
});
}
}Flexible key-value metadata storage on any model:
trait HasMetaData
{
public function setMeta(string $key, $value): void
public function getMeta(string $key, $default = null)
public function deleteMeta(string $key): void
}The REST API follows versioned namespacing:
/api/v1/auth/* → AuthController
/api/v1/bills/* → BillController
/api/v1/categories/* → CategoryController
/api/v1/transactions/* → TransactionController
/api/v1/teams/* → TeamController
/api/v1/notes/* → NoteController
/api/v1/webhooks/* → WebhookController
/api/v1/users/* → UserController
All responses follow a standardized envelope:
{
"success": true,
"message": "Operation successful",
"data": { ... }
}The application supports runtime feature flags stored in user metadata:
| Flag | Description |
|---|---|
enable_notes |
Enable / disable the notes module |
enable_calendar |
Enable / disable calendar view |
email_notification |
Email notification preferences |
web_notification |
Web notification preferences |
© Bill Organizer - Free for Personal use | Need paid license for Commercial use