Three years as the only developer on a live restaurant platform
Yukimura Sushibar runs on a Laravel application that I have built and maintained alone since October 2022. A database I inherited from 2024, a management panel I built on top of it, and three years of learning what happens to software after launch.
- When
- 2022 to present
- Where
- Yukimura Sushibar, Lisbon
- My part
- Sole developer, full stack
- Stack
- Laravel 10.48, PHP 8.1, MySQL
- Live at
- yukimura.pt
What it does
It is live at yukimura.pt, and has been in production the whole time it has been worked on.
The public side is four pages served by four controllers. The homepage, the menu, the events the restaurant runs, and the contact details. It covers more than one restaurant, so the visible ones are filtered rather than hard-coded, and the footer is composed from that same list instead of being written out by hand.
Behind it is a management panel where staff change all of that themselves. Menu categories and dishes, events, restaurants, homepage slides, cover images, scheduled tasks, user accounts, and an activity log that records who changed what. A small read-only public API serves the same content back out.
The database I inherited
The menu and event tables date from 2024 and were not written to Laravel conventions. The
events table is called event, its primary key is idEvent, and its
columns are evtName and evtprice. The menu side uses
id_menu, nome_prato and id_categoria.
I chose not to normalise any of it. Renaming columns on a live database to satisfy a convention buys nothing a customer can see and risks everything. Instead the models declare the reality explicitly, the table name, the primary key, the absence of timestamps, and the rest of the application is written as though that were normal.
One consequence is worth naming. evtName is forty characters in the schema, so
the validation rule is forty characters. Without that mirror the user gets a raw SQL error
instead of a message telling them what to fix. Every legacy column limit is reflected in a
form request for the same reason.
Tables I added later do follow Laravel conventions. There was no reason to make new work as awkward as the old work.
Security
This is the part that changed most once I started the Master's, and it is now the part of the application I am most confident about.
- Two-factor authentication is mandatory across the entire panel. The only route outside that requirement is the enrolment screen itself, and even that asks for the password again.
- Authorisation is decided by reversibility. Creating and editing are open to any active account, because they can be undone and they are recorded. Deleting needs an admin role and a reconfirmed password, because it cannot. The whole user management area sits behind the same bar.
- Password rules are defined exactly once, in a single service provider, after I found two different minimum lengths being enforced in two different places for the same credential.
- API tokens are stored hashed, never encrypted. Encryption is reversible, so for as long as the server could recover the value there would always be a path to it. They are shown once, at the moment they are generated, and rotated from the panel rather than from anyone's environment file.
- The API does not write. A shared token in a configuration file leaves no record of who used it, so every write goes through the panel, with a session, a second factor and a name attached.
- Every change is logged with the value before, the value after and the account responsible. That audit trail is what makes it reasonable to give staff edit rights at all.
- Security headers on every response, with a Content Security Policy applied to the panel specifically. The panel loads only its own CSS and JavaScript and has no inline scripts, so it can carry a strict policy. The public site depends on external libraries, and a policy dropped over that blindly breaks pages rather than protecting them.
- Sort columns never come from the request. The client sends a label, the controller looks that label up in a map it owns, and anything unrecognised falls back to the default.
The one I think about most is the token decision, because encrypting them would have looked equally responsible in a review and been meaningfully weaker.
No build step
The CSS and JavaScript are served straight from disk. There is no bundler in the request path, which is a decision rather than a gap. The site is four public pages and a panel, and a build pipeline would add a step that can fail during deployment without adding anything a visitor would notice.
The cost of that choice is caching. A stylesheet served directly stays in the browser of anyone who has already visited, and after a deployment that browser renders a broken layout. So every internal asset goes through a Blade directive that appends the file modification time as a query string. Choosing simplicity means owning the problem simplicity creates.
Testing
The feature tests run against an in-memory SQLite database and never touch MySQL. Permission tests always assert both halves, that an admin can do the thing and that a staff account gets a 403, because checking only the allow path tests nothing.
On top of that sits a set of browser tests, which exist for the questions a browser is the only thing that can answer. The interesting one is contrast. When text sits on a photograph, on a blur, or on a semi-transparent layer, the ratio you calculate from the CSS is simply wrong. Those tests sample the pixels the browser actually painted and measure from those. It is the only part of the front end where I write real comments, because a measurement nobody can explain is a measurement nobody should trust.
Deployment
Deployment is a git pull on shared hosting, with the served directory symlinked into the repository. That makes the main branch the site, not a branch that eventually becomes the site. A broken commit on main is a broken restaurant page the moment anyone deploys.
Which is why the workflow is stricter than the size of the project would suggest. Anything that passes through a state where the site does not work happens on a branch, and so does anything with a design document behind it. Small reversible fixes go straight to main, because pretending otherwise would be ceremony rather than safety.
Written rules instead of remembered ones
Working alone for three years, the thing that actually protects the project is writing decisions down. Not documentation in the abstract sense, but the specific rules that prevent a bad afternoon. Which commands must never run against the production database. Which directive to use for assets and why the obvious one is wrong. Which middleware a new panel route needs. Where the reasoning behind a design lives, so that it does not rot next to a rule that has since changed.
In six months I am the only person who can remember why something was decided, and I will not remember. The rules exist for that version of me.
What I took from it
- Inherited constraints are cheaper to describe honestly than to fight. The legacy schema cost me a few explicit lines per model and saved me a migration that could have taken the site down.
- Authorisation is easier to get right when the rule is a principle rather than a list. Reversible actions stay open, irreversible ones cost a second factor.
- Reading your own old code with a security mindset is the fastest way to learn what a security mindset actually is.
- Choosing the simpler architecture is allowed, as long as you take responsibility for the problem that simplicity creates.
- Being the only developer is not a reason to skip process. It is the reason to write one down.