A client portal for freelance web developers: approvals, messages, and invoices in one place.
🔗 Live (production): parlour.takumi.ca · 🎭 Try the demo: demo-customer-portal.takumi.ca
This repository is the actual source of the running application. Only secrets (credentials, keys) and real client data are excluded — the database schema, API, and business logic are all here.
The demo runs the same code as production, with fake data instead of real clients.
→ demo-customer-portal.takumi.ca/login
You don't need to type anything. The login screen shows two demo accounts, each with a button that fills in the credentials for you:
| Account | What you see |
|---|---|
| Admin view | The developer side: companies, clients, projects, task creation, invoicing |
| Client view | The client side: pending approvals, messages, invoices for their own company |
Sign in as one, look around, sign out, and try the other to see the same project from both sides. The demo database resets to a clean state every night, so feel free to change things.
Parlour is built around one job: the back-and-forth between a freelance developer and a client during a project. A developer sets up companies and projects, invites clients, and asks for the sign-offs that dev work actually needs — "is this design okay to build?", "does the staging site look right?", "can I deploy?". The client approves or rejects with a reason, sees invoices, and messages the developer, all in one place instead of scattered email threads.
- Token auth with Laravel Sanctum — SPA login, multiple devices, tokens revoked on logout.
- Two roles, kept apart —
developerandclient. Each sees a different set of screens, and the API enforces the split independently of what the UI shows. - Companies & clients — a developer manages companies and their contacts (a many-to-many model with a designated primary contact per company).
- Invitations — a developer invites a client by generating a link; the client opens it, sets a password, and their account is created. (The link is handed over directly; see Roadmap for email.)
- Projects — created by the developer; a client only ever sees projects belonging to their own company.
- Task approval workflow — tasks carry a type that matches real dev work (design review, staging review, deploy approval, dependency update). A client approves or rejects; rejecting requires a reason.
- Append-only approval log — every decision is written as a new immutable row. A later decision never overwrites an earlier one, so months later there is a clear record of what was signed off.
- Request queue (unlimited-requests model) — the client adds as many requests as they like (change, bug, new page, content, design, SEO), with an optional Loom-style screen-recording link, and reorders the queue themselves. The studio works from the top within a per-client active limit (e.g. one at a time), and the turnaround clock counts weekdays only in the client's time zone. Each request goes queued → in progress → client review → done, and a client can request unlimited revisions. Every request has a single timeline of comments and status changes; the studio can add internal notes and time logs that never reach the client's API, and track hours against an optional monthly allowance. A client can pause the queue instead of cancelling.
- Project brief — when a client opens a new project, an optional discovery questionnaire (business, goals, audience, budget band, deadline, references) is filled in up front.
- Testimonials & referrals — after approving a delivery, the client is invited to leave a rating and a testimonial (with explicit permission to publish) and to refer another business; the studio tracks each referral from new → contacted → won / lost.
- Messages — per-project threads (with replies) between developer and client.
- Whiteboards ("Image" menu) — a Miro-style infinite canvas built in-house: a dashboard of boards (templates, starring, grid / list) and an editor with sticky notes, shapes, text, frames, connectors, freehand pen, comments and image upload, with undo / redo and autosave. A board is private to its creator until it is shared with a project, at which point that project's clients can open and edit it too.
- Invoices — line items, tax, and a status (draft → sent → paid). A fixed-price job can be issued as two invoices in one step: a 50% deposit and a 50% balance.
- In-app notifications — a feed with read / mark-all-read, for events like a task waiting on the client.
- Phone notifications (PWA + Web Push) — add the portal to the Home Screen and get push notifications for messages, task reviews, requests, invoices, calls (incoming calls ring even with the app closed) and subscriptions. Each category can be switched on or off per user. Sending works on shared hosting (no queue worker or WebSocket server): pushes go out after the response is sent, and only to known browser push services.
- Bilingual UI (Japanese / English) — the language follows the browser, and can be switched by hand at any time.
Parlour is two separate apps that talk over HTTP:
api/— Laravel 13, JSON API only. No Blade views for the app itself; it returns data.web/— Vue 3 single-page app. It holds all the screens and calls the API.
Keeping them apart means the frontend and backend can be reasoned about, tested, and deployed on their own, and the same API could later serve a second client (a mobile app, say) without change.
Authentication. The SPA logs in against the API and receives a Sanctum token, which it sends on every later request. Logout revokes the token server-side.
Authorization is two independent checks, both server-side and both tested:
- Role (
role:developer) — who may manage companies, clients, and invitations. - Tenancy (
project.access) — which projects a given client may touch at all. Every route under/projects/{project}is guarded, and nested route bindings are scoped, so a{task}or{invoice}id from another company's project resolves to a 404 instead of being acted on.
Role alone is not enough: it would let any signed-in client reach another company's project by guessing an id. The SPA hides those screens from clients, but hiding a link is not authorization — the API checks it regardless.
Approval workflow and the append-only log. When a client approves or rejects a task, the decision is stored as a new row in the approval log. Those rows are never updated or deleted. This is the one place where "just overwrite the status" would have been easier but wrong: the history is exactly what a developer and client point at later when they disagree about what was agreed, so it is kept whole and is covered by tests.
Production and the demo run the same build. The only differences are configuration and data,
controlled by a single DEMO_MODE flag:
Production (parlour.takumi.ca) |
Demo (demo-customer-portal.takumi.ca) |
|
|---|---|---|
DEMO_MODE |
off | on |
| Data | real clients | fake data from a seeder |
| Login screen | plain | shows the one-click demo accounts |
| Nightly reset | no | yes — reseeded to a clean state daily |
Why do it this way. A portfolio needs a demo anyone can poke at, but the demo must never touch
real client data, and it must not drift into a different, half-maintained version of the app. So
instead of forking the code, there is one flag. The demo is the production build plus fake data,
which has a useful side effect: deploying the demo is a rehearsal for deploying production, on the
same script and the same host setup. A safety check in the seeder refuses to insert demo data
unless DEMO_MODE is on, so fake records can't land in the real database by mistake.
| Layer | Technology |
|---|---|
| Backend | Laravel 13, PHP 8.3 / 8.4 |
| Auth | Laravel Sanctum (SPA tokens) |
| Frontend | Vue 3 + Vite (Composition API) |
| State / routing | Pinia + Vue Router |
| i18n | vue-i18n |
| Styling | Tailwind CSS |
| Database | MySQL (production & demo); SQLite for the test suite |
.
├── api/ # Laravel 13 backend — REST API, auth, roles, approvals
└── web/ # Vue 3 + Vite single-page app
You need PHP 8.3+, Composer, Node, and a database (MySQL, or SQLite for a quick spin).
Backend (api/):
cd api
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serveFrontend (web/):
cd web
npm install
npm run dev # production-equivalent (no demo UI)The dev server proxies /api to the Laravel backend. To see the demo variant locally (the demo
banner and the one-click login accounts), run it with the flag on:
VITE_DEMO_MODE=true npm run devcd api
php artisan test # PHPUnit, SQLite in-memory — no database setup needed
vendor/bin/pint --test # code styleEvery push and pull request runs the suite on PHP 8.3 and 8.4, checks style with Pint, and
builds the Vue SPA — see .github/workflows/ci.yml.
The feature tests focus on the parts that are expensive to get wrong:
| Test | What it pins down |
|---|---|
AuthTest |
Sanctum token issuance, 401 on bad credentials, validation, password never serialised |
RoleAccessTest |
The developer/client split — clients cannot reach company-management routes |
ProjectAccessTest |
A client of company A cannot read or approve company B's project by guessing an id, and a task id from another project does not resolve |
TaskApprovalTest |
Only clients can approve or reject, rejection requires a reason, and the approval log is append-only — a later decision never overwrites an earlier one |
EnsureRoleTest |
The role middleware on its own, including the guest case |
These have groundwork in place (database tables, models, or dependencies) but are not wired up, so they are left out of the feature list above rather than half-claimed:
- File attachments — tables and models exist; no upload UI or API yet.
- Live message delivery — messages are sent and listed over the API today; real-time delivery (WebSockets) is not connected on the frontend yet.
- Email — invitations are shared as a link by hand; there is no email sending yet.
The rule for this public repository: code and schema are open; secrets and real client data are not.
- All secrets live in a single untracked
.envfile — seeapi/.env.examplefor the keys. - Real client data is kept out of version control; the demo uses a separate database with fake data.
- Application code, migrations, and logic are fully published — the implementation is the portfolio.
Built by Yoshiro Moriyama · Takumi Web Services


