# Generated app anatomy

What a Ripping app is made of, where it runs, where its data lives, and which scaffold it started from.

## What a generated app is

Every app Ripping builds is a Vite + React 19 + TypeScript project with Tailwind. It has no server of its own. Anything that needs one (accounts, saved data, uploads, payments, third-party APIs) goes through HTTP routes on Ripping under `/api/apps/<projectId>/`, and the app calls them with a session token.

The project starts from three layers:

| Layer | Where it comes from | What it holds |
| --- | --- | --- |
| Toolchain | `src/lib/template/vite-react.ts` | `package.json`, `index.html`, `vite.config.ts`, `tsconfig.json`, `src/main.tsx`, `src/index.css` (imports Tailwind), `src/design.css` (the colour and radius tokens) |
| Skeleton | `templates/skeleton/src` | `App.tsx`, `lib/*.ts`, `components/*`, `screens/*` |
| Scaffold (optional) | `templates/scaffolds/<key>/src` | Files layered on top of the skeleton for one kind of business |

The UI kit is not in the project. It is imported as `@ripping/kit` and resolves to a file Ripping serves at `/kit/kit-<hash>.js`. Its API is in [The kit](kit.md).

## The identity file

`src/lib/app.ts` is written once when the project is created:

```ts
export const APP_ID = "__APP_ID__";   // the project id
export const API = "__API__";         // the Ripping origin the app talks to
```

Every other `lib` module builds its URLs from these two. When `API` is a Ripping host, the bundler rewrites it to whichever origin is serving the app, so a preview and a published copy each talk to the origin that built them.

## The lib modules

| Module | Job | Page |
| --- | --- | --- |
| `lib/auth.ts` | Accounts: `useAuth`, sign in, sign up, Google, reset, profile, delete | [Accounts](auth.md) |
| `lib/store.ts` | Per-account data, cached in the browser and synced: `useStore`, `useSynced` | [Data](data.md) |
| `lib/collections.ts` | Declares shared collections and their rule | [Data](data.md) |
| `lib/shared.ts` | Records other people can see: `useShared` | [Data](data.md) |
| `lib/files.ts` | Uploads: `uploadFile`, `useFiles` | [Files](files.md) |
| `lib/pay.ts` | Products, checkout, purchases, subscriptions | [Payments](payments.md) |
| `lib/router.ts` | Hash routes: `useRoute`, `navigate`, `match`, `Link` | [Routing and SEO](routing-and-seo.md) |
| `lib/seo.ts` | `usePageTitle` | [Routing and SEO](routing-and-seo.md) |
| `lib/site.ts` | `SITE`: every public-facing string and the two shape switches | [Routing and SEO](routing-and-seo.md) |
| `lib/report.ts` | Crash reports and screen views | [Routing and SEO](routing-and-seo.md) |

Connected services (email, models, the owner's own Stripe, Google, a REST API) have no lib module; the app posts to the `call` route directly. See [Connected services](services.md).

## The one route table

`src/App.tsx` holds every screen of the app in `SCREENS`, written app-relative:

```ts
const SCREENS: { path: string; render: (params: Record<string, string>) => ReactNode }[] = [
  { path: "/", render: () => <Home /> },
  { path: "/settings", render: () => <Settings /> },
  { path: "/purchases", render: () => <Purchases /> },
];
```

`:id` segments arrive in `params`. A screen renders inside `<AppLayout>`, whose `NAV` list in `src/components/AppLayout.tsx` is the sidebar and the phone tab bar (the first five entries fit the tab bar). `PUBLIC` holds pages anyone can open signed out; the skeleton ships `/privacy` and `/terms`.

Two switches in `SITE` decide the shape of the whole app:

| `SITE.accounts` | `HAS_LANDING` | What `/` is | Where the app lives |
| --- | --- | --- | --- |
| `true` | `true` | The landing page when signed out, the app when signed in | `/` |
| `true` | `false` | Sign-in when signed out, the app when signed in | `/` |
| `false` | `true` | The landing page | `/app` (`APP_HOME`) |
| `false` | `false` | The app | `/` |

With accounts, a signed-out visitor who opens an app screen is sent to `/signin` and returned there after signing in. The first sign-in shows `<Welcome>` once (it sets the synced key `app.onboarded`). With no accounts there is no sign-in, no Welcome and no per-account data; what someone adds stays in their browser.

`App.tsx` wraps everything in `<KitProvider>` (toasts and confirms) and an `<ErrorBoundary>` that reports the crash and shows `<ErrorScreen>`.

## Preview and published

**Preview.** The editor bundles the project in the browser with esbuild-wasm and runs it in a sandboxed frame. Bare imports resolve to `esm.sh` (React pinned to 19.1.0; other packages take the version in `package.json`). Screen views and crash reports are not sent from the preview.

**Published.** Ripping bundles the same files on the server into `index.html` and `app-<hash>.js`, uploads them to object storage under `sites/<slug>/`, and serves them at `https://<slug>.<publish domain>`. The slug ends with the first six characters of the project id. A verified custom domain can point at the same files. `index.html` is served with `no-cache`; the script is immutable.

Both copies call the same API with the same project id. The project id is compiled into the JS of every published app, so it is not a secret; everything that matters is gated by the session token.

## Where data lives

| Data | Table on Ripping | Scope | Route |
| --- | --- | --- | --- |
| Accounts | `app_users` | Per project | `auth/<action>` |
| Per-account values (`useStore`, `useSynced`) | `app_data` | Per project, per user, per key | `data` |
| Shared records (`useShared`) | `app_records` | Per project, per collection | `records/<collection>` |
| Uploads | `app_files` + a private storage bucket | Per project, per owner | `files` |
| Payments and subscriptions | `app_payments`, `app_subscriptions` | Per project | `call` with `provider: "pay"` |

The browser also keeps a copy of per-account data under `localStorage` keys prefixed `data:<userId>:` (or `data:local:` when signed out), so screens open at once and keep working offline.

## Scaffolds

A scaffold is a whole starting point layered over the skeleton, chosen for a kind of business. The keys and blurbs are in `src/lib/scaffolds.ts`.

| Key | Label | What it adds |
| --- | --- | --- |
| `app` | App | Nothing beyond the skeleton: accounts, a home screen, settings, a landing page in front. |
| `lms` | Course platform | Screens for the catalogue, a course, a lesson, My learning, a certificate, and a Studio (courses, students, announcements). `lib/lms.ts` hooks (`useCourses`, `useCourseContent`, `useEnrollments`, `useProgress`, `useComments`, `useAnnouncements`, `isCreator`, `embedUrl`), `lib/types.ts`, `lib/seed.ts` sample courses shown until the creator adds real ones. Collections: `courses`, `modules`, `lessons`, `quizzes`, `questions`, `announcements` (public), `comments` (members), `enrollments` (inbox). Paid enrolment goes through `provider: "payments"` checkout with a Stripe price id on the course. The catalogue and course pages are open to signed-out visitors. |
| `internal` | Internal tool | `SITE.audience: "internal"`, `landing: false`, `indexable: false`. An Activity screen and `lib/activity.ts` (`logActivity`, `useActivity`, `describe`) on a `members` collection named `activity`. `lib/roles.ts` (`isAdmin`, `useRole`), `lib/csv.ts` (`toCsv`, `downloadCsv`, `parseCsv`, `pickCsv`) with `CsvButtons`, and `lib/shortcuts.ts` (`useShortcuts`). |
| `saas` | SaaS | Projects, Team and Billing screens. `lib/team.ts` (`useTeam`, `useTeamRecords`) on the `team` rule with `teams`, `team_members` and `projects` collections; invites are by email. `lib/plans.ts` (Free and Pro with limits) and `lib/billing.ts` (`useSubscription`, `usePlan`, `upgrade`, `manage`) over `provider: "payments"` on the owner's connected account. `UpgradeNudge` component. `SITE.pricing` is filled in. |
| `site` | Website | `SITE.accounts: false`. About and Contact pages listed in `lib/pages.ts` (`PAGES`, `CTA_PATH`), a `SiteFrame`, `ContactForm` and `NewsletterForm`. `lib/forms.ts` `submitForm(inbox, data)` posts to the `messages` and `subscribers` inbox collections without an account; the owner gets each one by email. |
| `marketplace` | Marketplace | Listed but `ready: false`: it cannot be picked and has no starter files yet. |
| `extension` | Chrome extension | Not a folder under `templates/scaffolds`; it comes from `src/lib/template/extension.ts`. A Manifest V3 extension: the popup is the app (380 px wide), `src/background.ts` is the service worker, `src/content.ts` an optional content script. `src/lib/ext.ts` wraps `chrome.*` (`useStored`, `getStored`, `setStored`, `removeStored`, `activeTab`, `openTab`, `sendMessage`, `onMessage`) and works in the preview through a shim. Packaged as a zip. |

A scaffold's `src/lib/collections.ts` replaces the skeleton's, so its rules are the ones Ripping enforces.
