Skip to content
Developer docs

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:

KindWhat it isWho can install it
appA 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.
templateA 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

  1. The owner picks a site and installsOne install per site for each app. Paid apps go through checkout first.
  2. The owner describes their business in one sentenceYour customization settings decide what that sentence may change: colors, text, form fields, layout.
  3. 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.
  4. 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.minor or major.minor.patch, up to 4 digits each, unique within the app.
  • A version is new (the app has never been live), an update (it has), or a resubmission (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, or customerImpactNone) and aiUpdateNotes (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.

FileHow manyTypesLargest
IconUp to 1PNG, JPEG, WebP, SVG2 MB
CoverUp to 1PNG, JPEG, WebP10 MB
Screenshots1 to 8 (at least one to submit)PNG, JPEG, WebP10 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:

your-repository/
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)
ezystudio-app.json
{
  "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"
}
FieldWhat it means
slugYour app's store slug. It names every folder your app owns.
templatesThe kinds of site it installs on: astro-static (Website), astro-app (App), or both. Owners can't pick a site of another kind.
routesUp to 8 page names your app adds (book → /book/). Lowercase letters, digits and dashes. Never api, site-api, account, portal, apps or __platform.
entryWhere visitors find it. Shown to the owner and their AI.
settingsOne 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.
dependenciesnpm 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.
sampleDataOptional. 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 siteForUse it for
src/apps/<slug>/**Every siteComponents, scripts, styles, your settings file
public/apps/<slug>/**Every siteImages and other static files, served at /apps/<slug>/
src/pages/<route>/**, src/pages/<route>.astroEvery siteThe pages for each route you declare
src/server/apps/<slug>/**App sitesYour Site API code (Hono)
src/pages/site-api/apps/<slug>/**App sitesThe 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 sitesDashboard screens
prisma/migrations/<14 digits>_app_<slug>_<name>/migration.sqlApp sitesYour 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.json at the top of the repository, or one whose slug isn'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.json or a lockfile in files/, 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.

sample-data.json
{
  "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_INVALID with meta.bundle.code bad_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.json and package-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.

WhenA file the customer didn't changeA file the customer changed
InstallWrittenThe 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 undoReplaced 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.
UninstallRemovedKept, 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.

No demo link to add

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.