TL;DR: A LingCode Cloud backend is a private Postgres database (plus auth, storage, realtime, functions, vector search) reached over HTTPS. You work in two layers: the schema (migrations, RLS, VIEWs, RPC functions โ full SQL, written once by the agent) and the browser SDK (per-table CRUD โ no JOINs, no upserts, 200-row pages). A mobile app, a web frontend, and a web API should all point at one backend and one schema; you separate users with row-level security, not with extra backends. Host the web build with the IDE's Cloud button (static โ lingcode.dev/apps/<id>, or full-stack โ <slug>.run.lingcode.dev). Get the conventions right up front and the agent builds the whole thing from a prompt.
Most LingCode Cloud docs cover one product at a time โ database, auth, hosting. This guide is the opposite: it walks the whole arc of shipping a real app โ backend, mobile API, web frontend, and web API โ and answers the question those pages don't: how do you keep several clients on one backend without their tables, routes, and data colliding? It's layered โ the first half is how to prompt the agent to do it; the second half is the hand-coding reference for when you want lower-level control.
When you're signed in, every project you open in the Mac IDE (or in /try) can reach a managed backend: a private Postgres database that LingCode operates for you, fronted by an HTTPS gateway. There's no connection string to manage and no server to run. The backend ships with:
Under the hood each backend is an isolated Postgres schema with its own database role. That isolation is a hard wall: one backend literally cannot read another's data. Remember that โ it's the reason behind the "one backend, many clients" rule below.
This is the single most important concept on the page. Everything else follows from it. There are two ways to touch your data, and they have different powers on purpose.
| Layer | Who writes it | What it can do |
|---|---|---|
| Schema layer (migrations, RLS, VIEWs, RPC functions, custom functions) |
The agent (or you), once, via migrations | Full Postgres. JOINs, CTEs, aggregates, upserts, indexes, constraints, generated columns, triggers, server-side SQL functions. |
| Browser SDK ( lingcode.from('table')โฆ, runs in your shipped app) |
Your app code, on every request | Per-table CRUD only. No JOINs, no upserts, max 200 rows per read. update and delete refuse to run without a filter. |
Put relational and heavy work in the schema โ define a VIEW or an rpc() function once, then read it from the browser like a plain table. Keep the browser SDK for simple reads and writes. If you find yourself wanting a JOIN or an upsert in app code, that's the signal to push it down a layer into a migration.
So the data API you call from a shipped app is deliberately small and safe. The full power of Postgres is still there โ it just lives one level down, in your schema, where the agent writes it once. The rest of this guide leans on that split constantly.
Here's the question this guide exists to answer: you want a mobile app, a web frontend, and maybe some web API routes. Do they each get a backend?
No. They share one backend and one schema. A "mobile API" and a "web API" on LingCode aren't separate servers you stand up โ they're the same gateway and the same tables, reached by different clients:
fetch against the same data URL).What keeps a user's data private isn't a separate backend per surface โ it's row-level security. Each signed-in user gets a session; the gateway pins reads and writes to that user's id; your RLS policies do the rest. The mobile app and the web app see exactly the same rows for the same user because they are the same rows.
When a client legitimately needs another backend's data (say, an admin tool reading a separate product), don't reach across โ expose it through a function or an HTTP endpoint on the owning backend and call that. The wall stays intact and the contract is explicit.
Because every client shares one schema, naming is what keeps them from stepping on each other. None of this is enforced by the platform โ it's the convention that makes a multi-client app stay legible.
snake_case, app-meaningful, no prefixes. Name a table workouts, not mobile_workouts or app_v2_workouts. The backend already lives in its own private schema, so you never add a backend prefix yourself โ that's generated for you. One table serves every client.user_id and an RLS policy. This is what lets web and mobile share a table safely:
CREATE TABLE workouts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE,
title text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE workouts ENABLE ROW LEVEL SECURITY;
CREATE POLICY workouts_owner ON workouts
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
user_id. A product_catalog or leaderboard everyone can read has a policy that allows reads to all and writes to none (writes happen server-side from a function).web-checkout, mobile-push-register, billing-stripe-webhook. The slug is the URL, so a clear slug is a clear endpoint.workout_feed, get_leaderboard(limit int). Several clients reuse the same one instead of each inventing its own.| Do | Don't |
|---|---|
One workouts table for web + mobile, separated by RLS | One table per client (web_workouts, mobile_workouts) |
user_id + an owner RLS policy on every private table | "We'll filter by user in the app code" |
Slugs like web-checkout / mobile-push | Two functions both called handler |
| A second backend only for a separate product | A second backend for the "mobile version" of the same app |
You rarely write the schema by hand โ you describe the app and let the agent build it against the live backend. It has direct tools for provisioning, migrations, and CRUD, and it works in a fixed order. A good prompt does three things: names the tables and who owns them, states the RLS intent, and says which logic must live server-side.
The backend's exact limits (tier, function timeout, whether long-running compute is available) are read live, not assumed. A reliable habit: start prompts with "check the backend's capabilities before designing" so the agent calls describe_backend and designs to what's actually there โ rather than reaching for localStorage or inventing an external service.
A prompt for the exact scenario in this guide โ one backend, web + mobile, no collisions:
Build a fitness app on the LingCode Cloud backend. First check the backend
capabilities, then provision it if needed and apply migrations.
ONE backend, ONE schema, shared by a web app and a mobile client.
Tables (snake_case, no prefixes):
- workouts (user-owned: user_id + owner RLS policy)
- goals (user-owned: user_id + owner RLS policy)
- leaderboard (public read, no user_id; writes only from a function)
Auth: email/password + Google sign-in. Use the built-in auth users table;
foreign-key user_id to it.
Server-only logic (put these in functions, not the browser):
- billing-stripe-webhook (verify + record payments)
- leaderboard-recompute (scheduled; writes the public leaderboard)
The web frontend uses the SDK directly; the mobile client hits the same
gateway. Do NOT create separate backends or per-client tables โ separate
users with RLS. Add indexes for the feed query, and expose the workout
feed as a VIEW so the client reads it without a JOIN.
Behind that prompt the agent follows a consistent sequence:
CREATE TABLE, RLS policies, indexes, VIEWs, and any RPC functions.You can also drive each step explicitly โ "add a notifications table owned by the user, with realtime enabled" โ and the agent migrates the schema in place. Schema changes are migrations, so they're versioned and repeatable rather than one-off edits.
The backend is one half; hosting the app is the other. In the Mac IDE the Cloud button in the toolbar builds your project and ships it to a live HTTPS URL. There are two tiers, and which one you get is detected from your project:
index.html). Served at https://lingcode.dev/apps/<id>. This covers Vite/React/Vue/Svelte SPAs and plain static sites.package.json: Next.js, SvelteKit, Nuxt, Astro, TanStack Start, or React Router 7. Served at <slug>.run.lingcode.dev, running on the edge. This is also where your web API routes live.What makes a project deployable, concretely:
web/, frontend/, apps/, and packages/. Keeping the web app in one of those keeps detection automatic.If you'd rather wire the SDK yourself โ in your own Next.js project, a plain HTML page, or a mobile client โ here's the lower-level surface. (Inside /try and the Mac preview this is all pre-wired as window.lingcode; you only do this when you take the app elsewhere. The dedicated walkthrough is Use LingCode Cloud in your own app.)
You need two public values from lingcode.dev/backends โ your backend โ connection details: the data URL and the anon key. The anon key is meant to ship in the browser โ access is enforced by RLS on the server, not by hiding the key.
<script src="https://lingcode.dev/sdk/lingcode-v1.js"></script>
<script>
const lingcode = LingCode.createClient(
'https://lingcode.dev/api/cloud/be/<your-backend-id>',
'<your-anon-key>'
);
await lingcode.ready; // settles any sign-in redirect first
const user = lingcode.auth.getUser(); // { id, email } | null
</script>
// read โ filters chain before the verb; returns { data, error }
const { data, error } = await lingcode
.from('workouts')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(50)
.select();
// write
await lingcode.from('workouts').insert({ title: 'Morning run', user_id: user.id });
await lingcode.from('workouts').eq('id', 1).update({ title: 'Evening run' }); // filter required
await lingcode.from('workouts').eq('id', 1).delete(); // filter required
Filter operators: .eq .neq .gt .gte .lt .lte .like .ilike .in(col,[โฆ]) .is(col,null), and .match({a:1,b:2}). Multiple filters AND together.
auth.signUp({email,password}) / signIn(...); passwordless sendMagicLink({email}); codes sendOtp + verifyOtp; social getProviders() then signInWithOAuth('google'). The SDK stores the session and attaches it to later calls, so reads run as the signed-in user and RLS keys off their id.await lingcode.storage.from('public').upload('avatars/me.png', file) โ data.url; getPublicUrl(path); download(path).const off = lingcode.from('workouts').subscribe(({type,row}) => { /* INSERT|UPDATE|DELETE */ }); call off() to stop. Events are RLS-filtered. Use it instead of polling.embedding column and rank by similarity with lingcode.vector.search({ table, column, embedding, limit, metric:'cosine' }).lingcode.functions.invoke('web-checkout', input) runs your server-side function (this is where secret keys are used).The browser SDK is CRUD-only, so when you need a JOIN, an aggregate, or an upsert, push it into the schema: create a VIEW and select() from it like a table, or write an SQL function and call it. Have the agent add it in a migration; then your app code stays simple and reads one "table." Mobile and web both reuse the same view โ no duplicated query logic.