studio.v1.0

Talk to your Laravel project. Approve what ships.

One Ubuntu server, one script, one wildcard DNS record. Studio gives a product team a human interface over Laravel workspaces that live on that server. Every conversation is the person's own Claude Code, running in their workspace with the Larapilot skills of the project: it writes the story, plans it, implements it, opens the pull request. The product manager reads, decides and approves next to the live preview of that branch. Nothing to install on a laptop but a browser.

The agent proposes. You approve what ships. Human-in-the-loop, always.

One server, one script

A fresh Ubuntu 24.04 or 26.04 VPS, a domain, an account on GitHub, GitLab, Bitbucket or Azure DevOps. The installer asks five questions and does the rest.

On the VPS, as root
  1. wget -qO- https://raw.githubusercontent.com/andreapollastri/studio/refs/heads/main/installer/install.sh | sudo bash
  1. The domain. You type it, for example dev.example.com. The script shows the one DNS record to create: *.dev.example.com → A → your server's IP.
  2. The check. It resolves a random name under the wildcard and studio.<domain> through a public resolver and waits until both point at the server. You can skip and fix DNS later; certificates are issued on demand when hosts resolve.
  3. The first administrator. Name and email. The password is generated: the summary shows it, with the dashboard's address and the email, in a yellow box before anything is installed, and again at the end. Save it then; nothing shows it later. Studio has no public sign-up and no social login: accounts are created here and in the dashboard, and people sign in with a password.
  4. The git provider. GitHub, GitLab (gitlab.com or your own), Bitbucket Cloud or Azure DevOps (with its organization): one for the whole Studio, the same forges Larapilot integrates with. See The git provider.
  5. Your token there. With the instructions on screen you create it; the script verifies it with the provider and stores it for your account. It clones and deploys project sites and creates new repositories. Every other person adds their own later.

Then it installs the newest release of Studio, the latest published vX.Y.Z tag (the head of the main branch only while no release is published; the first release then replaces it), with what it needs: Caddy, PHP 8.3, 8.4 and 8.5 (8.5 alone on Ubuntu 26.04, see below), Composer, Node, Claude Code, the provider's CLI (gh, glab or the Azure CLI), MariaDB, PostgreSQL and redis. Ten minutes later:

Open the dashboard
  1. https://studio.<domain>

Sign in, turn on two-factor authentication, add your Claude credential in Settings → Connections, create the first project in Administration → Projects, invite the team. From then on Studio updates itself every night to the newest release; see Updates.

Requires: Ubuntu 24.04 or 26.04 LTS (with sudo or Ubuntu 26.04's sudo-rs), amd64 or arm64, 4 GB of memory or more · a domain with a wildcard record · an account per person on the git provider · a Claude subscription or an Anthropic API key per person who chats · Laravel projects with Larapilot (Studio installs it, with Laravel Boost, in the repositories it creates).

What it installs

installer/install.sh turns a bare Ubuntu into the whole thing, and the same steps (in installer/lib/system.sh) run again at every update, so a release can bring what it needs. What apt prints goes to /var/log/studio-install.log; when a package fails, the installer shows the end of it. Run on a server where Studio is installed, the script starts the updater instead of asking questions; after an installation that stopped halfway it starts again from the questions. Beta testers install the head of the main branch instead of the newest release, and keep following it (see Updates):

Beta testers only
  1. wget -qO- https://raw.githubusercontent.com/andreapollastri/studio/refs/heads/main/installer/install.sh | sudo STUDIO_CHANNEL=beta bash
PieceRole
CaddyHTTPS for every host under the domain, with certificates issued on demand: before issuing one, Caddy asks Studio whether the host is known. No DNS API token, no per-host certbot runs.
PHP 8.3, 8.4, 8.5From ppa:ondrej/php, with the usual extensions and PHP-FPM. Each project picks its version; a pool per site and per workspace runs as its own user. The list comes from the release's installer/requirements.env, so a later release can add a version. Where the PPA does not serve the Ubuntu release yet (26.04 at first), PHP comes from Ubuntu's own archive, which carries PHP 8.5 alone: Studio runs on it and projects can pick it; the next update adds 8.3 and 8.4 once the PPA serves 26.04.
Composer, Node 22, Claude CodeWhat a Laravel project and the agent need. Claude Code is installed once, system-wide; each workspace runs it with its own login, and every update of Studio brings the newest one.
The provider's CLIgh for GitHub, glab for GitLab, the Azure CLI with the azure-devops extension for Azure DevOps; nothing for Bitbucket, whose REST API Larapilot calls directly. The agents open pull requests with it.
MariaDB, PostgreSQL, redisEach project picks an engine; every site and every workspace gets a database and a user of its own. Redis is shared with a prefix per site.
StudioThe newest release tag (or, for beta testers, the head of main) in /opt/studio/releases/<build>, with /opt/studio/current pointing at it and the .env and storage shared in /opt/studio/shared; built, migrated, served at studio.<domain>; a queue worker, a scheduler and Reverb as systemd services.
studio-adminThe privileged helper in /usr/local/sbin: the only thing the studio user may run as root, through sudo, for a fixed list of commands. Everything on the server is done through it.
studio-updateThe updater in /usr/local/sbin, run by root: nightly by a systemd timer, on demand from Administration → System. See Updates.
ufw, fail2banPorts 22, 80 and 443 open; brute force on SSH banned.

Where things land

/opt/studio/ current → releases/<build> the live build of Studio releases/<build>/ the last three: v1.2.0, or main-1a2b3c4 on the beta channel; owned by the studio user shared/.env, shared/storage/ what every release shares /etc/studio/ studio.env the domain, paths, Studio's PHP, the repository, AUTO_UPDATE, UPDATE_CHANNEL; root only update-signers optional: SSH keys allowed to sign release tags projects/<slug>.env the deploy token and database password of a site workspaces/<user>--<slug>.env one person's tokens for one workspace, readable by their Linux user only templates/ Caddy, PHP-FPM and systemd templates the helper renders /srv/studio/projects/<slug>/app a project site: user prj-<slug> /srv/studio/workspaces/<user>/<slug> a workspace: user ws-<user>, home /home/ws-<user> /etc/caddy/sites/*.caddy one host matcher per site and per workspace /etc/php/<ver>/fpm/pool.d/studio-*.conf /etc/systemd/system/studio-*.service Studio's queue, Reverb and scheduler; a bridge per workspace; queue workers per site and preview (Horizon, Reverb, Pulse when on) /etc/cron.d/studio-* schedule:run every minute, per site and per preview /var/lib/studio-update/state.json the updater's state and last log, read by the System page /var/lib/studio-update/repo.git a blob-less mirror of the repository: tags, main and which commit contains which /var/backups/studio/ the database dump taken before each update (the last five) /var/log/studio-install.log what apt printed during the installation and the updates /var/log/studio-update.log every update, in full

DNS and certificates

One wildcard record is enough: *.<domain> → the server. When a browser asks for maria-shop.<domain> for the first time, Caddy calls http://127.0.0.1:8081/api/tls/ask?domain=…; Studio answers 200 for its own host, a project slug or a workspace preview it knows, 404 for anything else, and Caddy issues the certificate. A host nobody created is never issued a certificate, which also keeps the rate limits quiet.

Trying it on your Mac first?

Studio runs without the server pieces: the local driver points every workspace at one bridge on your machine, driving the claude you already use. composer setup, an admin, composer run dev, then the bridge inside any Laravel project with Larapilot. The demo seeder gives you an agency with three projects and a live conversation. See Local development.

Five words

Everything in Studio is one of these.

Project

A Laravel repository with Larapilot installed, its deploy branch, the PHP version and database it needs, the staging site where the deploy branch runs, and the people who may open it. An administrator creates it; the repository is never touched by Studio itself.

Workspace

One clone per person × project on the server, under a Linux user of that person: the repository on the deploy branch, dependencies installed, a database of its own copied from the project site, Claude Code running with that person's credential, the bridge. Served at <user>-<project>.<domain>. A PM has one just like a developer: the skills that write the PRD or approve a story need a checkout to commit to.

Conversation

A Claude Code session inside the workspace, kept by Studio with its transcript and its session id, so it resumes days later with its memory. Several conversations can live in one workspace; each is a separate session.

Bridge

A small Node service inside the workspace. Studio sends it the person's message; it drives claude in stream-json mode, posts every event back to Studio, and holds a permission question until the person answers it. Zero dependencies, one process per conversation, released after an idle period.

Project site

The deploy branch, served by the same server at <project>.<domain>. Studio creates it when the project is created, registers a push webhook on the git provider, and redeploys it on every push to the deploy branch. Its Larapilot dashboard is what the PM watches; its API feeds the Board; its database seeds new workspaces.

Where things live

PlaceWhat runs thereWhat is stored there
BrowserStudio's interface: projects, the chat, and next to it the preview, the changes and the history, the Board, a terminal and an editor; the administrationNothing but the session cookie
The serverCaddy, Studio (Laravel 13 + Reverb), PHP-FPM pools, MariaDB and PostgreSQL, redis; one Linux user per person for their workspaces, one per project for its site; a bridge service per workspaceAccounts, projects, transcripts, permission decisions, encrypted tokens; the clones, the databases, each person's Claude login in their home
OutsideThe git provider: GitHub, GitLab, Bitbucket or Azure DevOps (repositories, pull requests, the push webhook); Anthropic (Claude Code with each person's subscription or key)Code, the tokens each person created

A request, from words to a pull request

Maria is the PM of Shop Acme. Here is one afternoon, screen by screen.

The Projects page: two project cards, one with a running workspace
Projects. Maria sees the projects she is a member of and the state of her workspace on each. Opening one creates the workspace the first time, a clone and a build in about two minutes; after that it is a click.
The chat with Maria's request and Claude's answer: a story written and planned
Ask in plain words. "The client wants to filter orders by date range. Can we have it by Friday?" Claude reads the PRD and the backlog through the Larapilot skills, writes story US-014, plans it, and answers with an estimate. The commands it ran are folded into one row each; Details opens them.
A permission card asking to run npm run build, with Allow and Deny buttons, the preview of the branch on the right
Approve what matters. Implementation starts on a branch. Before rebuilding the front-end assets, Claude asks: the PM's preset, Ask before actions, lets nothing outside its automatic scope run without her. One click, and the preview on the right shows the new filter on her own branch.
The Changes tab showing the branch, modified files and the diff
See the change. The Changes tab reads the working tree of the workspace: branch, files, diff. Its History and Branches views show the commit graph; the Files tab opens the same files in an editor.
The Board tab listing the backlog by status, read from the staging site
Keep the whole picture. The Board is the project's backlog as the project site publishes it: the same data the Larapilot dashboard shows, next to the conversation that changes it.

When the story is reviewed and merged on the deploy branch, the git provider calls Studio's webhook and the project site at shop-acme.<domain> pulls, builds and migrates. The Board reflects it at the next refresh.

What happened underneath

One message
browser→ Studio→ bridge→ claude -p --output-format stream-json

Studio records the message, marks the conversation as working, and hands the text to the bridge of that workspace together with the address and token to call back. The bridge writes one line on the agent's stdin.

Every event
claude→ bridge→ POST /api/bridge/events→ Reverb→ browser

Text, tool calls, results and permission questions come back as small batches. Studio stores them, broadcasts them to the open chat, and polls as a fallback when no websocket is available.

Maria — The client wants to filter orders by date range in the back office. Can we have it by Friday?

Claude — Yes. There is no story for this yet, so I'm adding it as a product request and planning it. US-014 · Date range filter on the orders list. Two tasks, half a day.

Maria — Perfect, go ahead.

Claude — Working on feature/US-014-date-filter. Before rebuilding the assets for the preview I'm asking you: Allow / Deny.

Four roles, one interface

A role decides whether a person can chat, which buttons they see, and how much Claude may do before asking.

RoleCanPermission presetButtons
AdministratorEverything: projects and their settings, MCP servers, people; a workspace on every projectacceptEditsStatus, Doctor
PMChat on their projects, approve; preview, changes and history, Board, terminal and files of their own workspacedefault: Claude asks before commands and edits outside the automatic scopeNew idea, Feedback, Status, Approve
DeveloperThe same as a PM, with the developer's preset and buttons: implement, test, open pull requestsacceptEdits: edits run, commands still askNext story, Plan, Open PR, Tests
ClientLook: the project site as preview and the Board of their projects; no chat, no workspace——

Presets map to Claude Code's --permission-mode and are set per role in config/studio.php. A conversation starts with its role's preset; Model & mode switches it among the modes the role may use (all four for administrators, PMs and developers). Custom skills the repository ships become buttons for everyone who can chat; see Buttons & custom skills.

One Claude per person. Every conversation drives the Claude Code signed in with that person's own plan, inside that person's workspace. Studio never holds a shared Anthropic account behind the interface; that is what the terms require and what keeps usage and cost attributable.

The git provider

One Studio works with one git provider, chosen in the installer: GitHub, GitLab, Bitbucket Cloud or Azure DevOps, the same remote forges Larapilot integrates with. Every project is a repository there and every person connects a token for it. Studio never mixes providers and keeps no history of a previous one.

ProviderRepositoriesA new one goes toThe agent's toolsPush webhook
GitHubhttps://github.com/owner/repoYour account or an organizationgh, with GH_TOKENRepository webhook, HMAC signature
GitLabhttps://gitlab.com/group/sub/repo, or your own GitLabYour namespace or a group / subgroupglab, with GITLAB_TOKEN (and GITLAB_HOST when self-managed)Project hook on the deploy branch, secret token
Bitbucket Cloudhttps://bitbucket.org/workspace/repoA workspace, or one of its projectsLarapilot's REST client, with BITBUCKET_ACCESS_TOKEN; no CLIRepository webhook on repo:push, HMAC signature
Azure DevOpshttps://dev.azure.com/org/project/_git/repoA project of the organizationaz repos (Azure CLI + azure-devops), with AZURE_DEVOPS_EXT_PATService hook on git.push, secret header

The installer asks for the provider, the address of a self-managed GitLab or the Azure DevOps organization, checks the administrator's token there and installs the provider's CLI. In every workspace the person's token is in the environment three ways: for git, through the askpass helper and with the user name the provider expects (x-access-token, oauth2, x-bitbucket-api-token-auth); for the CLI; and for Larapilot's integration. A repository Studio creates gets that integration turned on in its first commit (larapilot:settings-set --gitlab=YES and so on); in an existing repository the team turns it on once and commits .larapilot/config.yaml.

Changing provider. While Studio has no projects, php artisan studio:git-provider gitlab --url=https://git.example.com on the server switches it and forgets every token of the previous provider; sudo studio-update ensure then installs the new CLI. With projects the provider stays: another provider means another Studio.

GitHub

Organizations. A new repository goes to your account or to an organization you belong to; the form lists the organizations your token can see, and creating a repository there needs the right to do so in that organization. Organizations that restrict fine-grained tokens approve them in Organization settings → Personal access tokens. With SAML single sign-on, a classic token must be authorized for the organization (Configure SSO next to the token).

  1. Where. GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
  2. Resource owner. The organization that owns the projects, or your account. A fine-grained token reaches one owner: when projects live in several organizations, use a classic token with repo, read:org and admin:repo_hook.
  3. Repository access. The repositories of the projects, or all of them; administrators who create repositories need all of them.
  4. Permissions, everyone. Contents read and write, Pull requests read and write, Metadata read.
  5. Permissions, administrators. Also Administration read and write (new repositories) and Webhooks read and write (the deploy webhook).

GitLab

Groups. A new repository goes to your personal namespace or to a group or subgroup where you are at least Developer and the group lets that role create projects; the form lists those groups by their full path. A self-managed GitLab is set once, in the installer (STUDIO_GIT_URL=https://git.example.com), and the agent's glab gets it as GITLAB_HOST. Registering the deploy webhook needs the Maintainer role on the repository.

  1. Where. On your GitLab: avatar → Edit profile → Access tokens → Add new token.
  2. Scope. api: clone and push, merge requests through glab and Larapilot, and for administrators the new repositories and the webhook. read_repository and write_repository alone let git work, not merge requests.
  3. Expiration. GitLab asks for a date. Replace the token in Studio before it expires.
  4. Copy it into Settings → Connections. It starts with glpat-.

Bitbucket Cloud

Workspaces and projects. A Bitbucket repository belongs to a workspace and to one of its projects. The form lists the workspaces you belong to and their projects: a workspace alone puts the repository in its oldest project, a project puts it there. Creating repositories needs a workspace role that allows it; registering the webhook needs admin rights on the repository.

  1. Where. id.atlassian.com → Account settings → Security → Create and manage API tokens → Create API token with scopes → Bitbucket. API tokens replace app passwords: git signs in with the token as x-bitbucket-api-token-auth, the REST API with the token as a bearer.
  2. Scopes, everyone. read:user, read:workspace, read:repository, write:repository, read:pullrequest, write:pullrequest (each ends in :bitbucket).
  3. Scopes, administrators. Also read:project, admin:repository (new repositories), read:webhook, write:webhook and delete:webhook (the deploy webhook).
  4. Expiration. Up to a year. Replace the token in Studio before it expires.

Azure DevOps

Organization and projects. One Studio works with one Azure DevOps organization, set in the installer (STUDIO_GIT_URL=https://dev.azure.com/acme; acme.visualstudio.com addresses work too). Repositories live in the organization's projects: the form lists them, and a new repository follows the visibility of its project. Addresses of another organization are refused. Creating repositories needs the Create repository permission on the project; registering the push service hook needs Project Administrator.

  1. Where. https://dev.azure.com/<organization> → User settings → Personal access tokens → New Token.
  2. Organization. This Studio's, or all accessible organizations.
  3. Scopes, everyone. Custom defined → Code → Read & write.
  4. Scopes, administrators. Code → Read, write & manage, and Project and Team → Read.
  5. Expiration. At most a year, or less when the organization's policy says so.

The login on Azure DevOps is the account's e-mail address; the handle is its first part (maria.rossi@acme.com → maria-rossi).

A project

Adding a project is a form. Saving it clones the repository, creates its database, serves it at <slug>.<domain> and registers the push webhook on the git provider that redeploys it on every push.

Administration → Projects: a table with repository, stack, site status and members
Administration → Projects. Every project with its repository, stack, the site's state and the last deployed commit. Deploy redeploys by hand; the webhook does it on push.
The project form: name, slug, repository, deploy branch, PHP, database, members
The form. Six fields and the members. The slug is the hostname label of the site and of every preview.
FieldUsed for
Name, SlugThe slug names the site (shop.<domain>), the previews (maria-shop.<domain>), the Linux user, the directories and the databases. Lower-case letters, digits and dashes; derived from the name when empty.
RepositoryThe HTTPS address of a repository on the installation's provider: https://github.com/owner/repo, https://gitlab.com/group/repo (or your GitLab), https://bitbucket.org/workspace/repo, https://dev.azure.com/org/project/_git/repo. Addresses of another provider are refused. Cloned with your token for the site, with each member's token for their workspace.
Deploy branchWhat the site runs and what workspaces start from. A push to it deploys the site.
PHP, DatabaseA PHP version installed on the server (8.3, 8.4 or 8.5 today) and MariaDB or PostgreSQL, for the site and for every workspace of the project.
MembersWho may open the project. Administrators see every project without being members.

Or let Studio create the repository

The project form with Create it now selected: owner, repository name, private
Create it now. Pick where it goes, a name and whether it is private. Studio creates the empty repository on the git provider; the server installs the latest Laravel, Laravel Boost and Larapilot, commits, pushes the deploy branch, then provisions the site as for any other project.

Where a new repository can go depends on the provider: your account or one of your organizations on GitHub, your namespace or a group on GitLab, a workspace or one of its projects on Bitbucket, a project of the organization on Azure DevOps. Your token needs the right to create repositories there; the scopes are in The git provider. What lands in the new repository: composer create-project laravel/laravel at its current release; laravel/boost and andreapollastri/larapilot as dev dependencies, with larapilot:install and boost:install run (Boost is always there: if its install fails, the project stops with the reason instead of going on without it); Larapilot's integration for the provider turned on in .larapilot/config.yaml; and one commit on the deploy branch (plus main when the deploy branch is another name).

What an existing repository must have

  • Larapilot installed and larapilot:install run, so the skills, the .larapilot/ workspace, the dashboard and the API exist; Larapilot brings Laravel Boost with it, and boost:install gives the agent Boost's guidelines and MCP server. Without Larapilot the site still deploys, but there is no Board and the chat has no skills to call.
  • A .env.example. The server copies it and sets the application URL, the database, redis and the Larapilot API token; everything else you put there is kept.
  • Optionally a .mcp.json at the root for MCP servers every workspace should have, with secrets as ${ENV} references, and custom skills under .larapilot/skills/.

People

Accounts are created by an administrator and used with a password. No public sign-up, no single sign-on.

Administration → Users: name, email, role selector, git provider and Claude badges, project count
Administration → Users. Role is a dropdown; the badges say who has connected the git provider and Claude and who turned on two-factor authentication.
The New user dialog: name, email, role and an optional handle
New user. Name, email and role; the handle is optional and comes from the login on the git provider when the person connects.
  1. Create the account. Name, email, role. Studio shows a one-time initial password; share it once, it is never shown again.
  2. They sign in with email and password, change it, and turn on two-factor in Settings → Security: an authenticator app or a passkey.
  3. They connect the git provider and Claude in Settings → Connections. Until both are there, a project opens on a page that says so instead of a chat.
  4. They open a project. Studio shows the projects they are members of. The first open builds the workspace.
The login page: email, password, the invite-only notice
Sign in. Email and password, nothing else.

Git & Claude

Two credentials per person, stored encrypted, handed only to that person's workspaces.

Settings → Connections: the git provider token field with instructions, the Claude credential with its kind
Settings → Connections. The instructions are on the page, for the provider of this Studio (here GitHub): how to create the token, and claude setup-token or an API key for Claude.

The git provider

A personal token on the provider of this Studio, with the scopes in The git provider. Studio verifies it there, reads the login, and uses it as the person's handle: their previews are <login>-<project>.<domain>, their Linux user on the server is ws-<login> (on Azure DevOps the login is the e-mail address, and the handle its first part). Inside the workspace git pushes and the provider's CLI opens pull requests as that person. The token never sits in a file git can read: an askpass helper hands it to git from the environment, with the user name the provider expects.

Claude

Either a subscription or an API key. With Claude Pro or Max: on your own computer, signed in to Claude Code, run claude setup-token and paste the long-lived token. With an Anthropic Console account: paste the API key. The workspace exports CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY accordingly, and Claude Code's login files live in that person's home on the server, denied to the agent itself.

Changing a token updates every workspace the person already has and restarts its bridge. Removing one stops what depends on it: no git token, no new workspaces; no Claude, no chat.

The project site

<slug>.<domain> is the deploy branch, running on the same server. It is the project's staging, its dashboard and the source of every workspace's data.

  1. Creation. When the project is saved, the helper creates a Linux user, clones the deploy branch with the administrator's token, creates the database, writes the .env with APP_ENV=staging and a fresh LARAPILOT_API_TOKEN, runs Composer (dev dependencies included: the site is a staging environment, and Larapilot and Boost are dev dependencies whose dashboard and API it serves), npm, migrations, a PHP-FPM pool, a Caddy host and a queue worker. Studio registers a push webhook on the repository.
  2. Every push to the deploy branch reaches /api/git/webhook/<slug> with the project's secret: an HMAC signature from GitHub and Bitbucket, the secret token in a header from GitLab and Azure DevOps. Studio queues a deploy: fetch, reset to the branch, build, migrate, restart the worker. Deploy in the admin page does the same by hand.
  3. The Board reads https://<slug>.<domain>/larapilot/api/specs with the token the helper generated. The PM's bookmark is /larapilot on that host.
  4. New workspaces copy the site's database (a dump and a restore, MariaDB or PostgreSQL), so a person starts from realistic data. Without a site database, migrations run instead.
A client's view of the project: no chat, the project site as preview
A client's view. No workspace, no chat: the project site in the preview pane and the Board next to it.

Deploys are a reset to the branch and a rebuild in place, not atomic releases: a failed migration leaves the site on the previous code, a failed build may leave it mid-way for a moment. It is a staging site, kept simple on purpose. For production use the pipeline your project already has.

Project settings

What the server runs for a project, who may open its hosts, and which MCP servers its chats get. Settings on a project in Administration → Projects has three sections: Server, Access and MCP. Saving applies the first two to the site and to every workspace preview; MCP servers are saved one by one (see MCP servers).

The Server section of the project settings: scheduler, queues, Horizon, Reverb and Pulse
Project settings. The Server section; Access holds the rules for the hosts.

Server features

Scheduler
A cron line runs php artisan schedule:run every minute as the host's own Linux user. On by default, for the site and for every preview.
Queues
One queue:work worker per queue name, as a systemd unit that restarts on failure and on every deploy. default is on from the start; add mail, exports and two more workers appear.
Horizon
php artisan horizon in place of the plain workers: Horizon reads the queues from config/horizon.php and supervises them itself. The app must have laravel/horizon.
Reverb
Runs php artisan reverb:start on a loopback port of its own and proxies /app and /apps of the host to it. The helper writes the REVERB_* keys into the app's .env once; the app itself must have laravel/reverb installed.
Pulse
php artisan pulse:check, so the Pulse dashboard of the app shows the server's CPU, memory and disk. The app must have laravel/pulse.
HTTPS
Nothing to switch on: Caddy issues a certificate for every host the first time it is opened, the deploy site and each workspace preview alike (see The server).

Who can open the hosts

The Access section: the site open to Studio members with an office range whitelisted, previews public, one branch override
Access. The site for Studio members only, with the office network let through; previews public; a branch that only members may see.

Each project has a rule for its site and one for its previews, plus overrides for single branches. A rule is a mode and an IP whitelist:

Public
Everyone. With a whitelist, only those addresses.
Studio members only
The visitor must be logged in to Studio and be a member of the project (administrators always are). Anyone else is sent to the Studio login page and comes back after it. Whitelisted addresses pass without logging in.
Branch overrides
A rule for feature/secret applies to the site while that is the deploy branch, and to every workspace that is on that branch. Studio remembers each workspace's branch from its Changes tab.

How it works: when a project has any rule, its hosts get a Caddy forward_auth to Studio's /api/site-auth, which answers 204 (go on), a redirect, or 403. Hosts without rules are served directly, with no extra hop.

Studio's own session cookie never reaches a project host: the apps there run code the team writes, and must not see a cookie that opens Studio. A visitor without a pass is sent to studio.<domain>, logs in there if needed, and comes back with a one-time token valid for two minutes. The gatekeeper swaps it for a pass cookie bound to that host alone, good for twelve hours; it is useless on any other host and in Studio.

MCP servers

Give the chats of a project more tools: the project's documentation, error tracking, a CRM, an internal API. Any remote MCP server works; Studio does not care whose it is.

The MCP section of the project settings: two servers with their URL, roles and last test, and the form to edit one
MCP servers. Each with its URL, who gets it, its header and the answer of the last test.
Add one
Settings → MCP on the project: a name (it becomes part of the tool names, mcp__docs__search), the URL of the server, and usually one header such as Authorization: Bearer …. The value is stored encrypted and never shown again; editing without typing a new value keeps it.
Who gets it
Tick the roles. A PM can have the documentation server while only administrators and developers get the one that reads production errors. Clients get nothing unless you tick them.
Test
Studio makes the initialize call Claude Code would make, with the header, and keeps the answer: ok with the server's name and version, a refused header, a wrong URL.
In the chat
Each person's chat gets the enabled servers their role may use, from the next message on; Model & mode lists them. Their tools ask for permission like any other tool, unless the mode lets them through. Turn off takes a server out of every chat without forgetting it.

Remote here, commands in the repository

Studio only adds remote servers (the streamable HTTP transport). A server that runs as a command belongs in the repository's .mcp.json: Claude Code loads it in every workspace, as the person's own Linux user, with secrets as ${ENV} references. Both kinds end up in the same chat.

The bridge hands Studio's servers to Claude Code through a file only the workspace user can read, never on the command line, so a token does not show in the server's process list. It accepts nothing but {"type": "http", "url"} entries from Studio.

Updates

Studio keeps itself up to date. studio-update, run by root, installs every new build of its channel together with what it needs on the server: a new PHP, a newer Node, a package, the provider's CLI, a changed service. The stable channel follows the release tags; beta testers follow the main branch.

Administration → System: installed and newest release, Check now, Force update and Update buttons, the nightly switch, the channel, the last update with its log, the git provider
Administration → System. The installed build and the newest one, Check now, Force update and Update to, the nightly switch, the channel, the last run with its log, and the git provider of the installation.

Channels

ChannelInstallsFor
Stable (default)The newest release tag, vMAJOR.MINOR.PATCH; pre-releases and everything between two releases are skippedEvery team
Beta (beta tester mode)The head of the main branch, every new commit, before it becomes a releaseA server you test Studio on, not the team's daily work

The channel is a switch in Administration → System; the installer can start on beta (STUDIO_CHANNEL=beta, see What it installs). Neither channel goes backwards: a build is installed only when it contains the installed commit, which the updater checks on a blob-less mirror of the repository. Back on stable after beta, Studio keeps its main build, newer than the last release, until a release contains it; when a release is tagged on that very commit, it moves to the release's name.

Administration → System in beta tester mode: the newest commit on main, the beta channel selected and its warning
Beta tester mode. The newest commit on main instead of the newest release, each one installed as main-<commit> with the same backups and rollback as a release.

When

  • Every night, between 04:00 and 05:00, from a systemd timer, while automatic updates are on (the default).
  • From Administration → System: Check now, then Update to ….
  • Force update, in the same place: builds the newest build of the channel and installs it now, even when it is the one already installed (a broken build, a failed system step, a server changed by hand). The new build goes next to the live one as <build>@<time>, with the same dump and rollback. It never goes back to an older build: that is what studio-update rollback is for.
  • On the server: sudo studio-update apply (--force for the same as the button). Running the installer again does the same as apply.

What an update does

  1. Clone the build into /opt/studio/releases/<build>. A tag's VERSION file must say the same as the tag. With /etc/studio/update-signers in place, the tag (or, on beta, the commit) must carry a valid SSH signature by one of those keys.
  2. System step, as root, from the new build: its installer/requirements.env names the PHP versions, the PHP that runs Studio and the Node major; installer/lib/system.sh installs what is missing, refreshes Composer and Claude Code, and the provider's CLI. Nothing is removed.
  3. Upgrade scripts, phase before: installer/upgrades/<version>.sh of every version after the installed one up to the new one, in order, as root, for what a release needs beyond packages. On beta, where several builds share a version, a script that changed since it ran runs again.
  4. Build next to the running release, on the shared .env and storage: Composer without dev dependencies, the assets.
  5. Database: a dump to /var/backups/studio/, maintenance mode, migrations with the new code. Caddy's certificate question, the access check of protected hosts and the bridges' callbacks keep answering meanwhile.
  6. Switch: the new build's helper, templates, services, PHP-FPM pool and Caddyfile, then /opt/studio/current points at it; upgrade scripts, phase after; caches, restarts, maintenance off.
  7. Health check: Studio must answer /up. If it does not, or any step from the dump on fails, the previous release, its root files and the database dump come back, and the System page shows the log.

Project sites and workspaces are not touched: they are your projects. Bridges move to the new code when they restart (a workspace stopped and started, a changed token); a release that changes the bridge protocol restarts them in its upgrade script. Three releases and five database dumps stay on disk.

On the server, as root
  1. studio-update check # what the channel would install, and why not
  2. studio-update apply # --force · --to v1.1.0
  3. studio-update channel beta # or stable · auto on|off
  4. studio-update rollback # the previous release and its database
  5. studio-update ensure # the system step again · status

Releasing

For whoever maintains Studio: set VERSION (1.0.0 today), commit, and push a vX.Y.Z tag on a commit that passed the tests. Every stable server installs it at its next run; beta servers already run that commit. The release workflow refuses a tag whose VERSION says otherwise, and so does the updater. To sign releases, tag with git tag -s and an SSH key (gpg.format=ssh), sign the commits of main too if beta servers check signatures, and put the allowed signers, in ssh-keygen's allowed-signers format, in /etc/studio/update-signers on each server. What a release needs on the server goes in installer/requirements.env, or in an upgrade script (installer/upgrades/README.md).

The chat

Three panes: your conversations, the chat, and the right pane with the preview, the changes, the Board, the terminal and the files. The chat is Claude Code with a face.

The chat with Details on: every command and its result expanded
Details. Each tool call is a row with the command or the file; Details expands them all with their results, and shows the agent's reasoning when the model emits it.
Messages
Yours on the right. Claude's as prose, with Markdown rendered and code inline. System notices, in red when something failed, say what to do.
Streaming
Text arrives as it is typed over Reverb. Without a websocket the chat polls every few seconds while the agent works; nothing is lost either way.
Sessions
A conversation keeps its Claude session id. Close the laptop, come back tomorrow: the next message resumes with the same memory. The title is the first message, trimmed.
Interrupt
Stops the current turn. The process is released; the next message resumes the session.
Cost
The header shows the model and the cost of the last turn as Claude Code reports it.
Several at once
Open a new conversation for a separate topic. They share the workspace and the branch, so keep one piece of work per conversation.
The same conversation in dark mode
Light or dark. Studio follows the system appearance, or the choice in Settings → Appearance; the editor in the Files tab follows it too.

Permissions

Claude Code asks before running a tool its mode does not allow outright. In Studio the question is a card.

The card names the tool, shows the command or the file, and the reason when Claude gives one. Allow runs it with the input shown; Deny tells Claude the person declined, and the turn continues from there. While a question is open the conversation is marked waiting for permission in the sidebar, so a teammate glancing at the list sees who is blocked on whom.

PresetClaude Code modeWhat runs without asking
PMdefaultReads and the tools Claude Code auto-approves; edits and commands ask
Developer, AdministratoracceptEditsFile edits in the workspace; commands ask
ClientplanNothing: a client cannot chat, the preset only applies if the role changes

Bypass mode is disabled in every workspace by the managed settings. A question left unanswered when a turn ends is closed as denied. The decision, who took it and when, is stored with the conversation.

Model, effort, autopilot

Each conversation chooses how Claude Code runs. Model & mode in the chat header opens the dialog; the choice applies from the next message and the session keeps its memory.

The Model & mode dialog: model select, effort select, four modes
Model & mode. Model, effort and the permission mode of this conversation; below, the MCP servers this chat gets from the project settings.
SettingChoicesBecomes
ModelDefault, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 4.5 (the list is in config/studio.php)--model
EffortDefault, low, medium, high, max: how much the model thinks before it answers--effort
Ask before actionsClaude asks before commands and edits outside the automatic scope--permission-mode default
Ask, edits allowedFile edits in the workspace run; commands still askacceptEdits
AutopilotNo questions: Claude Code's classifier reviews each action before it runs; the turn goes to the end on its ownauto
Plan onlyReads, reasons and proposes; changes nothingplan

Which modes a role may pick is modes_by_role in config/studio.php; the role's starting mode is permission_mode. Bypass mode, the one with no review at all, is not offered and is disabled in every workspace by the managed settings: autopilot is as far as it goes.

Buttons & custom skills

The buttons above the input are prompts. Each one is sent to Claude as if typed; the Larapilot skill does the rest.

ButtonRoleSends
New ideaPM/larapilot-inception: the interview that produces a PRD and a backlog
FeedbackPM/larapilot-triage: bug or feature, into the backlog
StatusPM, AdminA read-only summary from larapilot:spec-list and larapilot:metrics
ApprovePM/larapilot-review: the review gate, approve or send back
Next storyDeveloper/larapilot-implement
PlanDeveloper/larapilot-plan
Open PRDeveloperPush the branch and open the pull request against the deploy branch
TestsDeveloperlarapilot:quality and the suite, summarised, nothing fixed without asking
DoctorAdminlarapilot:doctor and what to fix

Labels and prompts live in config/studio.php; {project} is replaced with the project name. Change them, add your own, remove the ones your team does not use.

Custom skills become buttons

Larapilot projects keep their own skills in .larapilot/skills/<name>/SKILL.md, committed with the code; skills in Claude Code's own .claude/skills/ count too. The bridge reads their front matter and Studio shows each one as a button after the role's own, with the description as the tooltip, for example the acme-predeploy-gate in the screenshots. Add one from the chat with /larapilot-custom-skill, as you would in the editor; commit, and every workspace has it at the next start.

The right pane

Where the conversation's effect shows up: Preview, Changes, Board, Terminal and Files. Clients see Preview and Board only.

Preview
The Laravel app of your workspace on your branch, served by the server at <you>-<project>.<domain>. Open puts it in a tab, which always works; inside the pane the app must not forbid framing (X-Frame-Options). While the workspace is stopped, and for clients, the pane shows the project site.
Changes
Branch, git status, the stat and the diff of the working tree, read through the bridge, plus the History and Branches views. It refreshes after every stored chat event while the tab is open.
Board
The backlog by status from the project site's Larapilot API, with code, title and priority. Cached a minute; Refresh reloads.
Terminal
Allowlisted commands in your workspace, without the agent in between; see Terminal.
Files
The workspace's files in the browser, with the editor of VS Code; see Files.
Over SSH
The workspace is a directory on the server. A developer with SSH access can open it in an editor over SSH; the same Claude session resumes there with claude --resume.

Files

A file manager and an editor for the workspace, in the right pane.

The Files tab: the workspace tree on the left, Order.php open in the editor with PHP highlighting
Files. The tree reads one folder at a time; the editor is Monaco, the component VS Code is built on.
Browse
Folders open on click and load through the bridge; .git is never shown; the button next to the open tabs hides the tree when the editor needs the room. New file, new folder, rename and delete sit in the row menu; a folder is deleted with everything in it, after a confirmation.
Edit
Files open in tabs. Syntax highlighting follows the extension (PHP and Blade, JavaScript, TypeScript, CSS, JSON, Markdown, YAML, shell, SQL…), with VS Code's find, replace, multi-cursor and keyboard shortcuts. The theme follows Studio's light or dark mode.
Save
⌘S / Ctrl+S or the Save button. Every file carries the hash of its bytes from when it was opened; if Claude or a terminal command changed it meanwhile, the save is refused and the tab says so instead of overwriting.
Limits
Binary files and files above BRIDGE_FILE_MAX (512 KB) are described, not opened. The editor is loaded on first use, so the rest of Studio stays light. Everything runs as the workspace's own Linux user, so what a person can edit here is exactly what they could edit over SSH.
The row menu of a folder in the Files tree: new file, new folder, rename, delete
The row menu. New file and new folder inside a folder, rename and delete for any row. A new name is typed in place.

History & branches

The Changes tab has three views: the working tree, the history, the branches.

The History view: a commit graph with lanes and merge lines, each commit with its refs, author and age
History. Every branch in one graph, with lanes and merges, refs as badges.
A commit opened from the History: message, author, date, the files it touched and the patch
A commit. Click one for its message, author and date, the files it touched and the patch.
The Branches view: local branches with the current one marked, a Switch button, and the remote branches with Fetch
Branches. Switch a branch and the preview and its access rule follow.
Working tree
Branch, git status, stat and diff of what is not committed yet; refreshed after every stored chat event while the tab is open.
History
The newest 80 commits of all branches in date order, drawn as lanes the way editors do: a branch opens a lane, a merge closes it. Refs show as badges (branches, origin/…, tags). Click a commit for its message, author, date, the files it touched and the patch.
Branches
Local branches with their upstream and ahead/behind, remote branches, Fetch. Switch checks a branch out in the workspace; the preview follows, and so does the access rule of that branch. Switch and Fetch act on your own workspace.

Terminal

A tab in the right pane runs commands in your workspace, as you, without the agent in between.

The Terminal tab: shortcut buttons, a command field, the output of git status
Terminal. Shortcuts for the useful commands, a field for the rest, the output as it arrives. The pane polls while a command runs; Stop kills it.

It is not a shell. The bridge takes the line, splits it into words, checks the binary and its first argument against a list, refuses anything that looks like shell syntax, and runs the binary directly in the workspace directory. Allowed today:

BinaryAllowedRefused
php artisan …Any artisan command with plain optionstinker, serve, db:wipe, down, migrate:fresh --force
composerinstall, update, require, remove, dump-autoload, show, outdated, audit, run, test, linteverything else
npmci, install, run …, test, outdated, audit, lseverything else
gitstatus, log, diff, branch, fetch, pull, checkout, switch, stash, show, add, commit, push, restore, remote, tag--force, -f, branch -D, force pushes
vendor/bin/…pest, pint, phpstan, phpunit, rectorother binaries
anyplain words, paths and optionsquotes, ;, &&, |, >, $(), backticks

Output is kept to 200 KB and a command is killed after ten minutes. The shortcuts come from config/studio.php; the allowlist lives in the bridge (bridge/src/commands.js), so what the dashboard sends is checked where it runs. Clients have no terminal.

The Terminal refusing a command that is not on the allowlist, with the reason under the field
Refused. A command that is not on the list, or looks like shell syntax, never runs; the reason shows under the field.

Subagents, plan mode, skills

The chat is the full Claude Code CLI running in the workspace. What works in the terminal works here.

A Plan only conversation: Claude hands two questions to subagents, shown indented, then answers with a table
Plan only, with subagents. "What is still missing for release 1.4?" Claude sends two subagents, one on the backlog and one on the open pull requests; their steps are indented under the call that started them. The answer is a plan; nothing was changed.
Subagents
When Claude delegates to a subagent, its messages carry the parent call and the chat shows them indented under a subagent label. Larapilot's personas and the autopilot worker use this.
Plan mode
Plan only in Model & mode: Claude reads and proposes, touches nothing. Switch to another mode when the plan looks right.
Skills and slash commands
Every /larapilot-* skill, the project's custom skills and any other skill in the repository. Type the command or use a button.
MCP servers
Those in the repository's .mcp.json are loaded, with secrets from the workspace environment, Laravel Boost and Larapilot's own server included; plus the remote servers added in the project's settings (see MCP servers).
Hooks
Larapilot's workflow hooks and Claude Code hooks run as they would in an editor; their output is kept out of the chat.
Not available
What needs a screen: images pasted into the prompt, the interactive command palette, voice. Interrupt replaces Escape.

Workspaces

A workspace is created the first time you open a project, started and stopped from the header, checked against the server every minute.

A project opened for the first time: no conversation yet, the workspace pill in the header
First open. The header pill follows the workspace: to create, creating, running, stopping, stopped, failed. A failed one shows the server's reason on hover.
  • Start / Stop start or stop the bridge and release the preview. Conversations and their sessions stay; a stopped workspace cannot receive a message, and the input says so.
  • What persists: everything, it is a directory and a database on the server: the clone, the dependencies, the Claude login in the person's home.
  • What does not: work that is not pushed, if the workspace is deleted. Keep branches short and pull requests small; the Larapilot loop wants that anyway.
  • Memory: PHP-FPM pools are on demand and a stopped workspace costs nothing; the Claude Code process is released after ten idle minutes and resumed on the next message.

Clients

A client account sees the projects it belongs to, the staging site as preview, and the Board. No chat, no workspace, no terminal or files, no cost.

It is the role for the person who pays: they follow what is in review and what shipped without opening another tool, and without an email thread. If a client should also talk to the project, give them the PM role; from then on they have a workspace and a Claude plan of their own.

Architecture

Laravel 13, Livewire 4 with Flux, Fortify, Reverb. A Node bridge per workspace. One bash helper with root.

browser ──WebSocket / HTTP──▶ Studio (Laravel) ──HTTP──▶ bridge (in the workspace) ──stdin/stdout──▶ claude -p ▲ ▲ │ └──── Reverb broadcasts ───────┴──── POST /api/bridge/events ──┘ per-workspace callback token Studio ──sudo──▶ studio-admin create sites and workspaces, deploy, start, stop (JSON in, JSON out) Studio ──HTTPS──▶ <slug>.<domain> GET /larapilot/api/specs, /metrics Git provider ──webhook──▶ Studio push on the deploy branch → deploy the site studio-update (root) ──▶ /opt/studio the channel's newest build: system step, build, migrate, switch, or roll back Caddy ──ask──▶ Studio may I issue a certificate for this host? Caddy ──forward_auth──▶ Studio may this visitor open this site or preview? (projects with access rules) Studio ──HTTPS──▶ MCP servers the Test button; the chats reach them from the workspace
App\Bridge\BridgeClient

Sends turns, permission answers and interrupts to the bridge, and tells it with every turn where to call back and with which token.

App\Bridge\IngestBridgeEvents

Turns the bridge's events into messages, permission requests, status changes and ConversationUpdated broadcasts. text_delta is broadcast only, never stored.

App\Workspaces\WorkspaceManager

Picks the driver: LocalDriver for development, NativeDriver for workspaces on this server through the helper.

App\Server\StudioAdmin, ProjectSites

The client of the helper, and the lifecycle of a project site: create, deploy, destroy, the push webhook.

App\Git\Providers

GitHub, GitLab, Bitbucket, AzureDevOps behind one GitProvider: who a token belongs to, where it may create repositories, the repository itself, the push webhook and how its deliveries are verified, the git user name and the variables the agent's CLI and Larapilot read.

App\Server\Updates

Reads the updater's state for Administration → System and asks the helper to check, to start an update (forced or not), to switch the channel or the nightly timer.

App\Larapilot\LarapilotApi

Reads the backlog and the metrics of the project site with the token the helper generated.

App\Server\SiteAccess, SitePass

The access rules of a host (mode, IP whitelist, branch override) and the one-time token and per-host pass that let a Studio member in.

App\Models\ProjectMcpServer, App\Mcp\McpProbe

The MCP servers of a project, with their encrypted header and roles, and the initialize call behind Test.

Jobs

ProvisionWorkspace, StartWorkspace, StopWorkspace, SendTurn, AnswerPermission, RefreshWorkspaces, ProvisionProjectSite, DeployProjectSite, ConfigureProjectSite, DestroyProjectSite, SyncWorkspaceCredentials on the database queue, so a clone or a build never blocks a page.

Events the bridge sends

EventStudio does
initKeeps the session id and the model; the conversation is working
text_deltaBroadcasts the slice to the open chat; nothing stored
text, thinkingStores an assistant message (reasoning hidden unless Details is on)
tool_use, tool_resultStores the call and folds the result under it
permission_requestCreates the pending request; the conversation waits
permission_resolvedMarks the request if it was still open; working again
resultIdle (or failed), keeps cost and usage, closes dangling questions as denied
errorA red notice; the conversation is failed until the next message
statusBroadcast only (rate limits, background task notes)
anything elseStored raw and hidden, so a renderer added later can show it

The bridge

A zero-dependency Node service in bridge/. It starts, per conversation, claude -p --output-format stream-json --input-format stream-json --include-partial-messages --verbose --permission-mode <mode> --permission-prompt-tool stdio, keeps stdin open so the next turn is one more line, and resumes with --resume <session> after the idle release. A control_request of kind can_use_tool is held until Studio answers; the answer is written back as a control_response.

MethodPathBody
GET/healthopen: version, workspace, sessions
POST/conversations/{key}/turns{text, session_id?, permission_mode?, model?, effort?, mcp_servers?, callback: {url, token}} → 202; mcp_servers is {name: {type: "http", url, headers}}, and a change starts a new process
POST/conversations/{key}/permissions/{request_id}{behavior: allow|deny, message?}
POST/conversations/{key}/interrupt—
GET/skillscustom skills from .larapilot/skills and .claude/skills
POST/run{command} → 202 with the run; refused with 422 when not on the allowlist
GET/run, /run/{id}recent runs, one run with its output so far
POST/run/{id}/killstop a run
GET/gitbranch, status, stat, diff of the working tree
GET/git/log?limit=, /git/branches, /git/commit/{sha}history with parents and refs; local and remote branches; one commit with stat and patch
POST/git/checkout, /git/fetch{branch} switches the working tree; fetch with prune
GET/files?path=, /files/read?path=one folder (folders first, .git hidden); a file's text with the hash of its bytes, or binary / too_large
PUT/files{path, content, hash} writes atomically; 409 when the file no longer matches hash
POST/files, /files/rename{path, type} creates a file or folder; {from, to} renames
DELETE/files?path=a file, or a folder with its contents; never the workspace itself

Every route but /health wants Authorization: Bearer $BRIDGE_TOKEN. Environment: BRIDGE_TOKEN, BRIDGE_PORT (4455), BRIDGE_HOST, WORKSPACE_DIR, CLAUDE_BIN, BRIDGE_IDLE_TTL (600 s), BRIDGE_FLUSH_MS (200), and the terminal and file limits in Configuration.

The protocol follows Claude Code's stream-json as documented by Spatie's Bloom. --permission-prompt-tool stdio is not in Anthropic's documentation: workspaces run Claude Code with auto-update off, so it changes only when you update it, and the bridge's tests, part of CI, run against a stand-in that speaks the protocol.

On the server

studio-admin is a bash script with root, allowed to the studio user through sudo for a fixed list of commands. Names travel as arguments and are validated against [a-z0-9-]; secrets travel as JSON on stdin; the answer is one JSON object. Studio calls it from queued jobs.

CommandDoes
project-bootstrap <slug>For a repository Studio just created: the latest Laravel, Laravel Boost and Larapilot, installed, committed and pushed.
project-create <slug>Linux user, clone with the deploy token, database, .env, build with dev dependencies, migrations, PHP-FPM pool, Caddy host, the services of the project settings (cron, queue workers or Horizon, Reverb, Pulse) and the access check when the project has rules. Returns the Larapilot API token and the commit.
project-deploy <slug>Fetch, reset to the branch, build, migrate, restart the services. Returns the commit.
project-configure, workspace-configureApply changed project settings to the site or to one preview: services, cron, Caddy host and access check.
project-delete <slug>Removes services, host, pool, database, user and files.
workspace-create <user> <slug>Linux user ws-<user> if new, the env file with the person's tokens, clone with their token, database copied from the site, .env, build, custom skills registered, pool, Caddy host, the bridge as a systemd service.
workspace-envRewrites the tokens (the git token and user name, the provider's variables for the agent, the Claude credential) and restarts the bridge.
workspace-start, -stop, -status, -deleteWhat they say. Stop also kills the person's processes in that directory.
update-check, update-start [force], update-channel, update-autoFor the System page: ask studio-update what the channel would install, start studio-update.service or studio-update-force.service (their own units, so they outlive the queue worker they restart), switch the channel and the nightly timer.

Two drivers in Studio: local points every workspace at one bridge on the developer's machine, for working on Studio itself; native is the one the installer configures. A workspace keeps the driver it was created with.

Security

  • Accounts: invite-only; email and password with two-factor and passkeys; roles enforced by policies on every page and action.
  • Previews and sites: served over HTTPS by Caddy. A preview runs with APP_DEBUG=true: close it with the access rules of the project (Studio members only, IP whitelists, branch overrides), which Caddy checks before the app sees the request. Studio's own session cookie never reaches a project host.
  • Bridge: listens on the loopback only, one port per workspace, with a token per workspace. Its callbacks carry a second token per workspace that Studio stores hashed and matches on every request.
  • Root: only studio-admin, through sudo, for named commands with validated arguments. Studio never runs shell commands of its own.
  • Secrets: git provider and Claude tokens, bridge and callback tokens, webhook secrets, MCP server headers and the Larapilot API token are encrypted at rest with the app key. On the server, each person's tokens sit in a root-owned file readable by their Linux user only, and git gets the token from the environment through an askpass helper, never from a file.
  • The agent: runs as ws-<user>; Unix permissions keep it out of other people's homes and out of the project sites; managed settings deny reading the login files and disable bypass mode. It is not a kernel boundary between two workspaces of the same person.
  • Updates: studio-update runs as root and installs the release tags of the repository in studio.env (and, on a beta server, the head of main). Whoever can push such a tag, or push to main for beta servers, can run code as root on every server that updates from it: protect the tags and the branch, sign them, and list the allowed keys in /etc/studio/update-signers so an unsigned or foreign build is refused. Turn the nightly updates off to read each release before installing it.
  • Transport: HTTPS everywhere; Studio's loopback listener on 8081 serves only the TLS question, the access check of protected hosts and the bridges' callbacks.

For zero public exposure put the server on a tailnet; then every person needs the client. Most agencies will keep Studio public with two-factor on.

Configuration

The installer writes /opt/studio/shared/.env, which every release links. The keys that are Studio's own:

VariableMeaning
STUDIO_DOMAINThe domain every host hangs under
STUDIO_WORKSPACE_DRIVERnative on a server, local while developing Studio
STUDIO_GIT_PROVIDER, STUDIO_GIT_URLgithub, gitlab, bitbucket or azure; the address of a self-managed GitLab, or https://dev.azure.com/<organization> (required for Azure DevOps). Change them only with php artisan studio:git-provider, while there are no projects
STUDIO_PHP_VERSIONSThe PHP versions a project can pick, kept by the installer and the updater in step with what is installed (8.3,8.4,8.5)
STUDIO_UPDATE_STATE, STUDIO_UPDATE_REPOSITORYWhere the updater leaves its state (/var/lib/studio-update/state.json); the repository the release links point at
STUDIO_HELPER, STUDIO_HELPER_SUDO, STUDIO_HELPER_TIMEOUTPath of studio-admin, whether to go through sudo, seconds a command may take (1200)
STUDIO_BRIDGE_PORT_BASEBridges listen on 127.0.0.1 at base + workspace id (42000)
STUDIO_CALLBACK_URL, STUDIO_BRIDGE_TIMEOUTWhere bridges post events: http://127.0.0.1:8081/api/bridge/events on a server; seconds Studio waits for a bridge (15)
STUDIO_LOCAL_BRIDGE_URL, STUDIO_LOCAL_BRIDGE_TOKEN, STUDIO_LOCAL_APP_URLThe one bridge and preview of the local driver
REVERB_*, VITE_REVERB_*Websockets for the live chat; Caddy proxies /app and /apps on the Studio host to Reverb
APP_LOCALEen by default; it ships in lang/it.json
BRIDGE_FILE_MAXBridge: largest file the Files tab opens or saves, in bytes (512 KB)
BRIDGE_RUN_TIMEOUT, BRIDGE_RUN_OUTPUT, BRIDGE_RUN_HISTORYBridge: seconds a terminal command may run (600), bytes of output kept (200 KB), runs remembered (20)

config/studio.php holds the role buttons, the permission preset and the allowed modes per role, the model and effort lists, and the terminal shortcuts. What changes per project (services, access rules, MCP servers) lives in the project settings, not in files. /etc/studio/studio.env holds what the helper and the updater need: the domain, the paths, the PHP that runs Studio, the repository releases come from (STUDIO_REPO), AUTO_UPDATE and UPDATE_CHANNEL (stable or beta).

Local development

Terminal
  1. composer setup && php artisan studio:make-admin
  2. composer run dev # server, queue, logs, vite · add: php artisan reverb:start
  3. cd ~/code/some-laravel-app && BRIDGE_TOKEN=local-bridge-token WORKSPACE_DIR=$PWD node ~/code/studio/bridge/bin/bridge.js

The bridge drives the claude on your machine with your login. Create a project in Administration, add yourself, open it: with the local driver the workspace is running at once and the chat talks to that bridge, no git provider or Claude connection needed. php artisan db:seed --class=DemoSeeder gives the agency of the screenshots, password password for everyone.

Checks
  1. composer test # pint, phpstan, pest
  2. cd bridge && npm test # node:test with a stand-in Claude CLI
  3. bash tests/installer/studio-update.test.sh # the updater in a sandbox