TL;DR: Load the SDK with <script src="https://lingcode.dev/sdk/lingcode-v1.js"></script>, create a client with your backend's data URL and anon key (copy both from lingcode.dev/backends โ your backend โ Settings / connection details), then call lingcode.from('todos').select(), .auth.signIn(...), .storage, and realtime. Inside /try the client is pre-injected as window.lingcode; in your own app you wire those two values yourself. One rule to remember: the data API is per-table CRUD, not raw SQL โ no JOINs, no upserts, 200 rows per page.
If you built an app in /try or the Mac IDE, the backend client was handed to you โ window.lingcode just existed. The moment you take that app somewhere else โ your own Next.js project, a plain HTML page, a Vite build โ you have to wire the backend up by hand. It's two values and one script tag, and this tutorial walks the whole thing, including the one design constraint that trips people coming from raw SQL.
A LingCode Cloud backend is a private Postgres database (plus auth, storage, realtime, and vector search) that you reach over HTTPS. You don't open a database connection or run SQL from the browser โ you talk to a small data gateway through the official client SDK, which is shaped like Supabase or Firebase so the calls feel familiar. Everything below is that SDK.
/tryEvery backend has exactly two things a client needs:
https://lingcode.dev/api/cloud/be/<your-backend-id>. This is the gateway for this backend.Find both at lingcode.dev/backends โ open your backend, and copy the connection details from Settings (the same pair you'd paste into another MCP client). In a framework, store them as public config โ for Next.js, NEXT_PUBLIC_LINGCODE_BACKEND_URL and NEXT_PUBLIC_LINGCODE_BACKEND_ANON_KEY; for a bundler, whatever your import.meta.env / process.env convention is. They're public, so they belong in client config, not in a server-only secret.
lingcode.functions.invoke(...)), which runs server-side. Anything the client can see, treat as public.
The SDK is a single zero-dependency file served from the CDN. The plain-HTML version:
<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>'
);
</script>
In a module/bundler app, load the same file and call LingCode.createClient(url, key) with your env values. (Inside /try and the Mac preview you skip this entirely โ the client is already there as window.lingcode, pre-wired to that project's backend. Never re-create it or hardcode keys there.)
Wait for the client to settle once on load before reading auth state โ it finalizes any sign-in redirect first:
await lingcode.ready;
const user = lingcode.auth.getUser(); // { id, email } | null
Every call returns { data, error } โ check error before using data. Filters chain before the verb:
// read
const { data, error } = await lingcode
.from('todos')
.eq('done', false)
.order('created_at', { ascending: false })
.limit(50)
.select();
// write
await lingcode.from('todos').insert({ title: 'Buy milk' });
await lingcode.from('todos').eq('id', 1).update({ done: true }); // filter REQUIRED
await lingcode.from('todos').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}) for several equals at once. Multiple filters AND together. update and delete refuse to run without a filter, so you can't wipe a table by accident.
lingcode.auth.signUp({email,password}) / signIn(...); passwordless sendMagicLink({email}) (the SDK auto-finalizes the returned link); email codes sendOtp + verifyOtp; social getProviders() then signInWithOAuth('google'). The SDK stores the session and attaches it to later calls, so subsequent .from() 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('todos').subscribe(({type,row}) => { /* INSERT|UPDATE|DELETE */ }); call off() on teardown. Events are RLS-filtered, so a signed-in user only receives their own rows. Use it instead of polling.embedding column and rank by similarity with lingcode.vector.search({ table, column, embedding, limit, metric:'cosine' }) for semantic search / RAG.This is the constraint that surprises people coming from a SQL client or PostgREST, and it's the difference between code that works the first time and code that 400s. The runtime data API operates on one table at a time. So:
user_ids, read those users, stitch them together client-side.ON CONFLICT. Insert, and if you get a duplicate-key error, fetch the row and update it instead..select() returns at most 200 rows โ page with .limit() / .offset(), and compute counts and sums in your code.Anything genuinely relational or heavy โ a multi-table report, an index, a constraint, a generated column, seed data โ belongs in the schema, not in a runtime call. Create a VIEW (then select() from it like a table), add the index, or run the migration from the console's SQL tab or the apply_migration tool. The gateway stays a thin, safe CRUD layer on purpose; the full power of Postgres lives one level down, in your migrations.
The SDK is intentionally Supabase-shaped, so most call sites map one-to-one. The two that don't are the two rules above: rewrite .select('*, author(*)') JOINs into separate fetches, and .upsert() into insert-then-update. In the Mac IDE or /try, just ask the agent to "migrate my Supabase project to LingCode and deploy it" โ the migration skill does the schema, the data, and these rewrites for you.