Year one of a SaaS product is the easy part. You know every table by heart, every decision is still warm in your memory, and the backlog is whatever you and one other person agreed on last Tuesday. The code is small enough that reading it is the documentation.

Year two is where it gets interesting. Someone joins. A client's project manager wants a status update in a tool you don't use. A bug lands on a feature you shipped eleven months ago and you can no longer reconstruct whether the behaviour is intentional or a regression. And if you've been delegating implementation to an AI agent — which by now most of us have — you discover the agent's memory of your product resets every session, while the product's complexity does not.

The instinct is to fix this with discipline: write better docs, keep the board tidy, be more rigorous in review. It never holds. Discipline is a renewable resource for about three weeks.

What actually holds is putting the product's intent into the repository, in a form that survives session resets, staff changes, and your own forgetting — and then giving the humans who don't read repositories a window onto it. That's the whole thesis of this article, and it breaks into three concrete layers.

Three questions a SaaS has to keep answering

Strip away the tooling and every long-lived product has to answer the same three questions, repeatedly, for years:

What is this supposed to do? The product contract. Requirements, priorities, what is deliberately out of scope. Usually it lives in someone's head, a stale Notion page, and a Slack thread from March.

What are we building right now? Delivery state. Which stories exist, what's planned, what's in review, what shipped. Usually a board that drifts from the code within a fortnight.

What exists, and who owns it? The organisational picture. Which services exist, which APIs they expose, who to page, where the docs are. In a solo project this question is invisible. At fifteen repositories it's the only question that matters.

The three answers want to live in different places, and that's fine — the failure mode is when they contradict each other and no one can tell which one is lying. So the architecture that works is: one source of truth, in git, next to the code; and read-only windows onto it for the audiences that need a different surface.

Concretely, in a Laravel context: Larapilot for the first two questions, Linear as the window for stakeholders, Backstage as the window for the organisation.

Layer one: the product contract lives in the repo

I built Larapilot to solve the first two questions for Laravel projects. It's a Composer package that installs a spec-driven workflow on top of Laravel Boost — the agent proposes, you approve, and everything either of you decided lands in git as a file.

composer require andreapollastri/larapilot --dev
php artisan larapilot:install
php artisan boost:install

That's the setup. What it gives you is a .larapilot/ directory in your repository containing a PRD, a backlog, one YAML file per user story, a technical plan per story, review notes, and support intake. Skills — /larapilot-inception, /larapilot-spec, /larapilot-plan, /larapilot-implement, /larapilot-review and a handful of others — are published into your editor through Boost and orchestrate the conversation that produces those files.

The workflow itself is unremarkable, and deliberately so. You describe the product once, it becomes a PRD with MoSCoW-tagged functional requirements. Must and Should requirements become user stories. Each story gets planned, implemented against the plan, and then stops at a human review gate before it can be marked done. A state machine enforces the order — you cannot implement something that was never planned, and you cannot approve something that was never implemented.

What matters for longevity isn't the loop. It's where the loop's output goes.

Git as the agent's long-term memory

Here's the thing about AI-assisted development that catches people out around month six: the agent has no memory of your product. Every session it wakes up knowing general Laravel and nothing about the eleven decisions you made in April. If your product's intent lives in chat history, it's already gone.

When the PRD, the spec, the technical plan and the review feedback are files in the repository, the agent reloads all of it at the start of every skill invocation. The context isn't recalled — it's read. And because it's in git, it's diffable, reviewable in a pull request, and blamed like any other file. A year later, git log .larapilot/docs/PRD.md tells you when a requirement changed and what triggered the change.

This is also why the PRD is treated as a living document rather than a founding artifact. It carries a revision history, and it's updated selectively:

What happened PRD updated? Where it lives
New capability requested Yes — new FR or scope change PRD + spec + revision history
Routine bug No Support intake + fix spec
Bug reveals a missing requirement Yes — clarify the parent FR PRD bullet + rework on the story
Review sends work back No Review feedback on the spec

That distinction is the maintenance discipline in miniature. Most bugs are not requirement changes and should not touch the product contract — but the Safari-only SSO failure that nobody ever specified is a requirement gap, and quietly patching the code hides that fact from everyone who reads the PRD next year.

Dials that let the process age with the product

The second thing that makes a workflow survive is that it can be turned down. A process calibrated for a funded team of eight is intolerable on a side project, and the reverse is negligent.

Larapilot exposes five persistent settings, written to .larapilot/config.yaml and honoured by every skill: effort (how deep the agent works, from token-economical to full sub-agent passes), backlog granularity (one spec per user journey, per capability, or per requirement), git mode (no Gitflow ceremony, Gitflow without push, or Gitflow with push and PR updates), testing (critical-path only, standard feature tests, or the full bar with browser E2E and a viewport matrix), and auto-approve (whether the human review gate can ever be skipped).

Early on, a solo MVP is honestly better served by lean granularity, minimal testing, and no branch ceremony. Once there's a client, revenue, and a second developer, you turn the same dials up without changing tools or re-learning a workflow. That's a small thing that turns out to matter a lot: the reason processes get abandoned is usually that they were sized for a different phase of the product and nobody could adjust them incrementally.

Day two: features and bugs get their own doors

Greenfield inception is a one-time event. Everything after it arrives through two shortcuts, and this is where a spec-driven setup earns back its cost.

/larapilot-feature "Add PDF export for invoices" runs a mini-inception: it checks the existing PRD and backlog for duplicates, asks about MoSCoW priority, whether it extends an existing requirement or introduces a new one, complexity, epic placement, and blocking dependencies. Out comes a new story traced to a requirement — not a task floating in a board with no lineage.

/larapilot-bug "SSO login fails on Safari" runs triage instead: severity, environment, whether it's security-relevant, whether it maps to an existing story. When the environment is production or staging and the report mentions errors, it pulls a diagnostics snapshot — health checks plus a redacted log tail, with secrets stripped — and cites that evidence in the intake log. Then it routes: rework on the existing story, or a new fix spec.

The value here is boring and cumulative. Eighteen months in, support/intake.md is a genuine record of what broke and why, each entry linked to the story it came from, rather than a Slack archive nobody can search.

Layer two: Linear, and giving stakeholders a window

Everything above is for people who open the repository. That is not everyone who has a legitimate interest in your product.

What Linear actually is

Linear is a commercial issue tracker built for software teams, and it's opinionated in a way that's worth understanding before you integrate anything with it. Work is organised into teams, each with a short key — ENG, PLAT — which prefixes every issue in that team (ENG-42). Issues move through workflow states that the team defines, typically something like Backlog → Todo → In Progress → In Review → Done. Issues can have sub-issues, an estimate, a priority, labels, and a parent project or cycle — Linear's timeboxing unit, roughly a sprint with less ceremony.

Its design philosophy is speed and opinion: keyboard-driven, fast, and deliberately less configurable than Jira. Product managers and clients tend to genuinely like it, which is precisely the problem it creates for you. They live there. They are never going to open backlog.yaml.

The integration, and the one rule that makes it safe

Larapilot's /larapilot-tracker skill mirrors the backlog into Linear (or Jira, Asana, Trello, ClickUp, or Monday) over an API key. Stories become issues carrying the spec body, priority, points as the estimate, and the epic. Plan tasks become native sub-issues, not a checklist buried in a description.

LARAPILOT_TRACKER_ENABLED=true
LARAPILOT_TRACKER_PROVIDER=linear
LARAPILOT_LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxxxxxx
LARAPILOT_LINEAR_TEAM=ENG

The rule that keeps this from becoming a second, competing workflow is that the direction is deliberately asymmetric. Push is authoritative: the repository decides what a story says and which column it belongs in. Pull is a report — it tells you what drifted and changes nothing until you explicitly pass --apply.

php artisan larapilot:tracker-status --ping   # credentials + target board
php artisan larapilot:tracker-push --dry-run  # what would change, no API calls
php artisan larapilot:tracker-push            # backlog → Linear
php artisan larapilot:tracker-pull            # drift report, read-only
php artisan larapilot:tracker-pull --apply    # write mapped statuses back

Four boundaries are worth internalising, because they're the difference between a useful window and a mess:

Done is never applied from a tracker. A PM dragging a card to Done is an opinion, not an acceptance. Done is a human review gate that records the merge commit, and it stays with /larapilot-review. The pull reports the drift and refuses to act on it.

Spec text is never read back. Titles, bodies and acceptance criteria are owned by the repository. Edits made in Linear get overwritten on the next push, and the card says so in its footer. This sounds harsh until the first time a well-meaning stakeholder rewrites an acceptance criterion in a way that silently contradicts the tests.

A status outside the map is reported, never guessed. If someone invents a new column, you get a visible unknown rather than a plausible-looking wrong answer.

Credentials never enter the repo. The tracker key is write-capable on a third-party workspace, so it lives in .env and CI secrets. What does get committed is .larapilot/tracker.yaml — the story-to-issue-id map, identifiers only. Commit it, or your colleague's first push creates a duplicate ENG-… for every story you have.

Expect the first real push to fail, by the way. It almost always does, and for the same reason: the client's team calls its first column Backlog, not Todo. Larapilot will not create columns in someone else's tracker — it fails, names the states that do exist, and leaves you to either rename the state or point the map at reality:

'status_map' => [
    'TODO' => 'Backlog',      // was 'Todo'
    'PLANNED' => 'Backlog',   // same column — not drift
    'IN PROGRESS' => 'In Progress',
    'REVIEW' => 'In Review',
    'DONE' => 'Done',
],

Note the second line. TODO and PLANNED mapping to one column is normal and is explicitly not treated as drift — otherwise you'd get a false alarm every single morning, and a drift report you learn to ignore is worse than no drift report.

Once it's configured, put the push in CI on the default branch. Unchanged stories are fingerprinted and skipped without an API call, so a push on every merge costs essentially nothing:

- name: Sync backlog to Linear
  run: php artisan larapilot:tracker-push
  env:
    LARAPILOT_TRACKER_ENABLED: true
    LARAPILOT_TRACKER_PROVIDER: linear
    LARAPILOT_LINEAR_API_KEY: ${{ secrets.LARAPILOT_LINEAR_API_KEY }}
    LARAPILOT_LINEAR_TEAM: ENG

Layer three: Backstage, and the organisational picture

The third question — what exists and who owns it — doesn't appear until you have more than a couple of repositories. Then it appears all at once.

What Backstage actually is

Backstage is an open framework for building developer portals. It was created at Spotify, open-sourced in 2020, donated to the CNCF, and reached Incubation level in 2022; several thousand companies run it today. The problem it was built to solve is specific and recognisable: as Spotify scaled into the thousands of microservices, engineers could no longer find the APIs they were supposed to be using.

Four pieces make up the core. The Software Catalog is an inventory of everything the organisation runs — services, websites, libraries, APIs, data pipelines — each registered as an entity with an owner, a lifecycle, and metadata. TechDocs builds documentation sites from Markdown living beside the code, using MkDocs, and renders them inside the portal, so docs sit on the same page as the thing they document. Software Templates scaffold new components along your organisation's golden paths. And a large plugin ecosystem wires in CI, monitoring, cloud providers, and the rest.

An entity is registered by committing a catalog-info.yaml at the root of the repository. That file is the contract between your repo and the portal.

The philosophy is worth quoting because it explains the shape of the integration: centralise, don't standardise. Backstage doesn't force every team onto the same toolchain — it gives you one place to see everything, whatever it's built with.

Repo-level meets org-level

Larapilot is repo-level; Backstage is org-level. /larapilot-backstage publishes one into the other. It asks for the owner, the system and the lifecycle, then generates a catalog entity, an mkdocs.yml, and a TechDocs site built from your PRD and backlog.

php artisan larapilot:backstage-export           # preview — writes nothing
php artisan larapilot:backstage-export --write   # generate catalog + TechDocs

The resulting entity looks like this, with the description pulled from the PRD's elevator pitch and annotations pointing back at the workspace:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: acme-shop
  title: 'Acme Shop'
  description: 'Rental management for equipment hire companies.'
  annotations:
    backstage.io/techdocs-ref: 'dir:.'
    larapilot.io/prd: .larapilot/docs/PRD.md
    larapilot.io/board-url: 'https://staging.acme.test/larapilot'
  tags: [laravel, larapilot]
spec:
  type: service
  lifecycle: production
  owner: 'group:default/platform'
  system: commerce
  providesApis:
    - acme-shop-api

Set the owner properly — this is the one field people leave on the default and regret. An unresolvable owner shows up as a dangling reference in the catalog, and the entire point of the catalog is knowing who to ask.

Each user story is rendered to a TechDocs page: status, priority, points, epic, task progress, then the spec body and the technical plan. Internal feedback bodies stay in the repository; only counts are published, because a portal is not the place for half-finished review arguments.

There's also a live snapshot endpoint — metrics, per-status counts, blocking feedback, and a story list without spec bodies, so a portal plugin can poll many repositories cheaply. Two cautions there, both non-negotiable: call it through the Backstage backend proxy so the token stays server-side, and never point it at production. The API returns 404 in production by design. If the portal can't reach a dev or staging environment, ship the committed catalog-info.yaml and TechDocs instead — they're regenerated in CI and are perfectly sufficient.

When it's honestly not worth it

If your organisation doesn't already run Backstage, skip this entirely. Standing up a portal for one Laravel repository is a genuinely bad trade — the read-only dashboard Larapilot already serves at /larapilot covers a single repo perfectly well.

Backstage becomes worth it at a specific threshold: when other people's services are already in a catalog and yours being absent is the problem, or when you have several Larapilot repositories and want one org-wide view of delivery state. Below that line it's ceremony.

Putting the three together

The architecture that results is simple enough to state in one sentence: the repository decides, and everything else renders.

                    .larapilot/  (git — source of truth)
                    PRD · backlog · specs · plans · reviews
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
        /larapilot        Linear          Backstage
        (dashboard)     (stakeholders)   (organisation)
         read-only      push authoritative   read-only
                        pull reports drift

Which means the direction rules are worth writing on a wall somewhere:

Change Where it happens
Product scope PRD, via inception or the feature skill
Technical approach The plan, via the plan skill
Acceptance The review gate — human, always
Status Skills and Artisan; the tracker may suggest, never decide
Ownership and lifecycle Backstage config in .env, not the workflow config

The last row catches people out. Catalog identity describes the organisational picture, not the delivery workflow, so it belongs in Laravel config and .env — not in .larapilot/config.yaml alongside effort and testing modes. Mixing them means your delivery settings and your org metadata drift apart the first time someone reorganises teams.

What this actually looks like eighteen months in

Concretely, in a project running this way for a year and a half:

The PRD has a revision history with maybe fifteen entries, each naming what triggered the change — a feature request, a bug that revealed a gap, a topology decision. Anyone, including an agent that woke up thirty seconds ago, can read the current contract and the path that led to it.

The backlog has a hundred-odd stories, each traceable to a requirement, each with the technical plan that was actually executed and the review notes from when it was accepted. When a bug arrives on a feature from last spring, you don't reconstruct intent from the code — you read what it was supposed to do.

The client's PM has been watching progress in Linear the whole time and has never once asked you for a status update, because the board updates itself on every merge. When they drag something to Done prematurely, nothing breaks; you see it in the morning drift report and finish it properly.

And when a second engineer joins, the onboarding is: read the PRD, read the catalog entry, run /larapilot-spec and watch what happens. Not three days of you talking.

The honest limits

None of this makes the product decisions for you. A PRD full of vague requirements produces vague stories and vague code, faster than before. The tooling enforces structure, not judgment.

It also doesn't eliminate the review burden — it relocates it. You review intent before implementation instead of diffs after it, which is cheaper and more effective, but it's still your attention being spent. The auto_approve: YES setting exists, and I'd encourage you to think hard before turning it on for anything a customer touches.

And there's a real maintenance cost to the artifacts themselves. A backlog that stops matching the code is worse than no backlog, because it's confidently wrong. The state machine and the CI sync exist precisely to make drift visible rather than silent, but they can't make you care.

What you get in exchange is that the expensive knowledge — why this works this way, what we decided not to build, who owns this thing, what state the work is actually in — stops living in your head and in chat logs, and starts living in the same place as the code, versioned alongside it.

That's not a productivity trick. It's just the difference between a product you can still reason about in year three and one you can only add to.


Larapilot is open source under MIT — larapilot.web.ap.it · GitHub