Building apps

Generated app anatomy

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

View as markdown

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:

LayerWhere it comes fromWhat it holds
Toolchainsrc/lib/template/vite-react.tspackage.json, index.html, vite.config.ts, tsconfig.json, src/main.tsx, src/index.css (imports Tailwind), src/design.css (the colour and radius tokens)
Skeletontemplates/skeleton/srcApp.tsx, lib/*.ts, components/*, screens/*
Scaffold (optional)templates/scaffolds/<key>/srcFiles 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.

The identity file

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

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

ModuleJobPage
lib/auth.tsAccounts: useAuth, sign in, sign up, Google, reset, profile, deleteAccounts
lib/store.tsPer-account data, cached in the browser and synced: useStore, useSyncedData
lib/collections.tsDeclares shared collections and their ruleData
lib/shared.tsRecords other people can see: useSharedData
lib/files.tsUploads: uploadFile, useFilesFiles
lib/pay.tsProducts, checkout, purchases, subscriptionsPayments
lib/router.tsHash routes: useRoute, navigate, match, LinkRouting and SEO
lib/seo.tsusePageTitleRouting and SEO
lib/site.tsSITE: every public-facing string and the two shape switchesRouting and SEO
lib/report.tsCrash reports and screen viewsRouting and SEO

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.

The one route table

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

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.accountsHAS_LANDINGWhat / isWhere the app lives
truetrueThe landing page when signed out, the app when signed in/
truefalseSign-in when signed out, the app when signed in/
falsetrueThe landing page/app (APP_HOME)
falsefalseThe 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

DataTable on RippingScopeRoute
Accountsapp_usersPer projectauth/<action>
Per-account values (useStore, useSynced)app_dataPer project, per user, per keydata
Shared records (useShared)app_recordsPer project, per collectionrecords/<collection>
Uploadsapp_files + a private storage bucketPer project, per ownerfiles
Payments and subscriptionsapp_payments, app_subscriptionsPer projectcall 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.

KeyLabelWhat it adds
appAppNothing beyond the skeleton: accounts, a home screen, settings, a landing page in front.
lmsCourse platformScreens 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.
internalInternal toolSITE.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).
saasSaaSProjects, 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.
siteWebsiteSITE.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.
marketplaceMarketplaceListed but ready: false: it cannot be picked and has no starter files yet.
extensionChrome extensionNot 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.

Generated app anatomy · Ripping docs