(01)
Three sides, one booking
Locandwork is an Airbnb-style marketplace for workspaces. Customers search and book. Hosts list a space and split it into workspaces — hot desks, dedicated desks, private offices, meeting rooms — each with its own capacity and hourly, daily and monthly price in dirhams. Admins approve or reject listings before anyone can book them.
I did all of it: the schema, the API, the Vue app, and the "Linen & Forest" look with its own logo.
routes under /api/v1
Eloquent models
Vue components
migrations

(02)
No double bookings — enforced by the database
Checking availability and then inserting is a race: two people can pass the check at the same moment. So the check isn't in PHP. Every booked night becomes a row in booking_days, and that table has a unique index on workspace + day.
// database/migrations/…_create_booking_days_table.php // same workspace cannot have two bookings on the same day $table->unique(['space_workspace_id', 'day']);
The booking service locks the workspace row, creates the booking, then inserts one row per night. If any night is taken, the insert fails, the whole transaction rolls back, and the customer gets a clean 422 instead of a half-made booking.
// app/Services/BookingService.php (trimmed) return DB::transaction(function () use (…) { SpaceWorkspace::where('id', $workspace->id)->lockForUpdate()->first(); $booking = Booking::create([…]); try { DB::table('booking_days')->insert($rows); // one row per night } catch (QueryException $e) { if ($this->isDuplicateKey($e)) { throw ValidationException::withMessages([ 'start_date' => ['One or more of these dates are no longer available.'], ]); } throw $e; } return $booking; });
Two small decisions make it hold up. Check-out day isn't inserted — the hotel model — so back-to-back stays don't collide. And the duplicate-key check recognises both MySQL's error 1062 and SQLite's message, so the same guard works in production and in tests.
(03)
Tools for hosts
Hosts create a listing with all of its workspaces in one form — one database transaction, so a listing never exists without its desks — then upload photos and wait for approval. Incoming bookings arrive as pending; the host confirms or cancels from a single table.


(04)
Small things that add up
- Slugs everywhereSpaces resolve by slug on every route group — public, host, admin and wishlist.
- Idempotent wishlistSaving twice never throws; a unique pair plus
syncWithoutDetaching. - Token authSanctum bearer tokens, one Axios instance, a 401 interceptor that signs you out.
- Route guardsVue Router rehydrates the user and sends each role to its own dashboard.
- Loading statesSkeleton shimmers and empty states on every list, in the brand's own tones.
- Realistic seedNine Casablanca spaces, two hosts, eleven bookings — the screenshots here use it.
(05)
What's next
Stripe Connect so hosts get paid out automatically, a messages UI on top of the threads table that already exists, reviews after a stay, and a proper edit flow for listings. Hourly bookings currently block the whole day — true per-hour slots are the next change to the booking service.