Building an app
An App Store app is a ready-made feature a business owner adds to their EzyStudio site in one click: bookings, a store, a client portal. It arrives working, with sample content, and the owner makes it theirs with one sentence.
What an app is
Every listing in the App Store is one of two kinds:
| Kind | What it is | Who can install it |
|---|---|---|
app | A feature that installs into an existing site: pages, server routes and database tables, for example bookings with deposits. | Free: any plan. Paid: the App and Business plans. |
template | A whole website template sold in the store. | Free: any plan. Paid: the Website, App and Business plans. |
On top of that, an app only installs on the plans you list in its plans.
EzyStudio sites are built from two templates, and apps are built the same way, as changes to a site's code:
- Website (
astro-static): Astro, every page prerendered, no server code and no database. - App (
astro-app): Astro with a server (a Hono app at/site-api), the site's own logins and roles, and its own Postgres database. Anything with accounts, payments or stored data needs this kind of site.
Build your app against these templates and their rules. The rules every coding agent follows are published on each site at /api/public/site/v1/agent-docs (see Site API).
How an app installs into a site
- The owner picks a site and installsOne install per site for each app. Paid apps go through checkout first.
- The owner describes their business in one sentenceYour
customizationsettings decide what that sentence may change: colors, text, form fields, layout. - EzyStudio adds your files to the site's codeYour pinned version's bundle is checked again, then its files are written into the site's working copy in the editor and committed to the site's git history. The owner's sentence goes to their own AI to apply. Nothing goes live until the owner publishes.
- The owner sees each setup stepYour
setupSteps(the first 6). Without them: “Adding the app to your site”, “Applying your hours, prices and details”, “Matching your brand colors”, “Finishing up”.
An install stays pinned to the version it got. When you publish a new version, owners can update, and undo an update. A customer's own changes stay theirs: see Updates, undo and removal.
Versions
- Your app always has one draft you edit. A new app starts at version
1.0. - A version you submit is frozen. After a decision, your next edit creates a new draft from the latest version with the next minor number (
1.0→1.1,2.3.4→2.4). You can set the number yourself:major.minorormajor.minor.patch, up to 4 digits each, unique within the app. - A version is
new(the app has never been live), anupdate(it has), or aresubmission(after changes were requested). - While a version is in the queue, the draft is locked. Withdraw it to keep editing.
- A live app stays live while an update is being reviewed, sent back or rejected. Approval publishes the new version straight away.
- Every update after the first says what changed:
changelog(what's new, shown on your listing),customerImpact(what customers might lose, orcustomerImpactNone) andaiUpdateNotes(instructions for the AI that applies the update to each customer's customised copy). See Every update says what changed. - The version number you set is a proposal. It must be higher than the live version; the reviewer who publishes it confirms it, or sets another, when it goes live.
Screenshots and media
Listing images must be uploaded to EzyStudio, so the listing always shows exactly what was reviewed. Links to images hosted elsewhere are refused.
| File | How many | Types | Largest |
|---|---|---|---|
| Icon | Up to 1 | PNG, JPEG, WebP, SVG | 2 MB |
| Cover | Up to 1 | PNG, JPEG, WebP | 10 MB |
| Screenshots | 1 to 8 (at least one to submit) | PNG, JPEG, WebP | 10 MB each |
- Alt text on every image is required to submit (up to 250 characters). Say what the screenshot shows.
- Screenshots appear in the order you send them. The cover is the big image on your store card and listing page (the form suggests 1920 x 1080).
- An upload URL is valid for 15 minutes and accepts exactly the size and type you declared.
- SVG icons are served so they display as images but never run as a page.
The bundle
The bundle is your app's code for this version. It lives in a GitHub repository: connect GitHub under App code in the Media step of the submit form and pick the repository and branch (saved as repo in the manifest). When you submit, we freeze the branch's latest commit and keep that copy: it is what gets installed. The repository follows one layout, from its top folder:
ezystudio-app.json the bundle manifest (required to install files)
files/<site path> files added to every kind of site you support
files.astro-static/… extra files for Website sites only
files.astro-app/… extra files for App sites only
APP.md notes for the owner's AI: how to use and customize your app (optional, 16 KB)
sample-data.json rows for your review test site's database (optional, App sites, 256 KB)
package.json read by review only, never installed (with package-lock.json, index.html){
"schema": 1,
"slug": "salon-booking",
"templates": ["astro-static", "astro-app"],
"routes": ["book"],
"entry": "/book/",
"settings": "src/apps/salon-booking/settings.json",
"dependencies": {"date-fns": "4.1.0"},
"sampleData": "sample-data.json"
}| Field | What it means |
|---|---|
slug | Your app's store slug. It names every folder your app owns. |
templates | The kinds of site it installs on: astro-static (Website), astro-app (App), or both. Owners can't pick a site of another kind. |
routes | Up to 8 page names your app adds (book → /book/). Lowercase letters, digits and dashes. Never api, site-api, account, portal, apps or __platform. |
entry | Where visitors find it. Shown to the owner and their AI. |
settings | One file under src/apps/<slug>/ that holds your colors, text, fields and layout options. Read them from there, so a customization is a data change, not a code change. |
dependencies | npm packages with exact versions (20 at most). EzyStudio doesn't install them: they are listed for the owner's AI, which adds them before it builds. The security check reads your bundle's package.json, not this list, so list them there too. |
sampleData | Optional. A JSON file at the top of the bundle with rows for your own tables. Reviewers see them in your test site. Never installed in a customer's site. See Sample data for review. |
Where your files may go
| Path in the site | For | Use it for |
|---|---|---|
src/apps/<slug>/** | Every site | Components, scripts, styles, your settings file |
public/apps/<slug>/** | Every site | Images and other static files, served at /apps/<slug>/ |
src/pages/<route>/**, src/pages/<route>.astro | Every site | The pages for each route you declare |
src/server/apps/<slug>/** | App sites | Your Site API code (Hono) |
src/pages/site-api/apps/<slug>/** | App sites | The one route file that mounts it at /site-api/apps/<slug>/*. Keep the sameOrigin and csrf guards from src/platform/guards. |
src/portal/apps/<slug>/** | App sites | Dashboard screens |
prisma/migrations/<14 digits>_app_<slug>_<name>/migration.sql | App sites | Your tables, named app_<slug>_… (dashes become underscores) |
Your app never edits the site's own files (its menu, home page, layouts, src/server/app.ts, schema.prisma). Linking to your pages, or adding your models to the schema, is the owner's AI's job: say how in APP.md.
What is refused
The code is checked when you pick the branch (the draft refuses it with 422 E_BUNDLE_INVALID, with the reason and the file), again on the commit we freeze when you submit, and again, from the stored copy, every time an owner installs. It is refused when it has:
- no
ezystudio-app.jsonat the top of the repository, or one whoseslugisn't your app's; - a file outside the paths above, or a path with
.., a leading/or odd characters; - a hidden file or folder among the files you install (
.git,.git/hooks,.env,.npmrc,.husky…), or a symbolic link anywhere in the repository; - a file that runs code outside your app:
package.jsonor a lockfile infiles/, build or worker config (astro.config.*,vite.config.*,wrangler.*), shell scripts, native binaries; - anything in the managed core (
src/platform/,docs/agent/); - a file you install that looks like it holds a secret: a private key, or a live key or token for Stripe, AWS, GitHub, Slack, Google, SendGrid, an AI provider or EzyStudio;
- more than 500 files to install, a file over 2 MB, more than 20 MB in all, more than 2,000 files and folders in the repository, or a repository over 50 MB.
We keep the commit and a SHA-256 of its frozen copy from the moment you submit. An install whose stored copy no longer matches it is refused: the version a reviewer approved is the version owners get, whatever you push to the branch later. Nothing in a bundle ever runs on EzyStudio. Your files run only inside the customer's site, after they publish.
Sample data for review
Every reviewer tests your app on a throwaway test site with its own database. Your migrations run on it like on a customer's site, then we fill it with sample customers and the template's starter records. Ship sample data so your own screens aren't empty when the reviewer looks.
sampleData names a JSON file at the top of the bundle. It is rows, not SQL: an object of table names, each with a list of rows. Tables are filled in the order you write them, so put parent tables first.
{
"app_salon_booking_services": [
{"name": "Haircut", "minutes": 45, "price_cents": 3500}
],
"app_salon_booking_bookings": [
{"service_id": 1, "customer_name": "Maya Rahman", "starts_at": "2026-11-02T10:00:00Z"}
]
}- Only tables your own migrations create. Never the site's tables (users, roles, sign-in, jobs).
- Table and column names are plain names (letters, digits, underscores). Values are text, numbers, true or false, null, or JSON for JSON columns.
- 256 KB at most, 20 tables, 1,000 rows, 40 columns a row, 10,000 characters a value. No secrets.
- Checked when you submit: a file that breaks these rules answers
422 E_BUNDLE_INVALIDwithmeta.bundle.codebad_sample_data. - We write every insert ourselves and pass your values as data, so nothing in the file runs as SQL.
- If a row doesn't fit your tables (a missing column, a wrong type), nothing is loaded and the test site fails with the reason. The reviewer sees it, so try your file on your own Preview Database first.
What review reads
The automatic review checks also read three files from the frozen commit (the shallowest match, outside node_modules):
package.jsonandpackage-lock.json: dependencies are checked against known security advisories, install scripts and dependencies from git or URLs.index.html: used for the accessibility, pricing and data checks until the test site is live.
Updates, undo and removal
Every install writes a record next to your files, .platform/apps/<slug>.json, with the version and an id for each file as it was written, and adds your app's section to INSTALLED_APPS.md (your APP.md, the owner's sentence, where the files are), which the owner's AI reads before it touches your app. Each change is one commit, App Store: install Salon Booking 2.4.
| When | A file the customer didn't change | A file the customer changed |
|---|---|---|
| Install | Written | The install is refused if any of your paths already holds a file you didn't write: “Your site already has files where this app goes”. Nothing is changed. |
| Update or undo | Replaced with the new version (or removed, if the new version dropped it) | Merged with the new version when the merge is clean. Otherwise theirs is kept and yours is saved in .platform/apps/<slug>/incoming/ for their AI to merge. A file they deleted is put back. |
| Uninstall | Removed | Kept, and listed in a note for their AI so the site still builds |
A site that hasn't opened its editor yet gets your files the moment it does. If anything fails half-way, every file is put back as it was and the owner sees one plain sentence. A failed update or undo keeps the version they had.
We host the preview ourselves: the commit you submit is installed into a fresh test site. Reviewers open it, and the automatic checks load it to measure speed and scan the page. Until it is live, those two checks wait instead of failing.