Linabase Documentation

Learn how Linabase works, integrate the SDK, use the REST API, or connect your ORM. Self-hosting is on the roadmap.

Concepts

Before diving into code, here is how Linabase is organized and what the key building blocks are.

How Linabase Works

Linabase gives you a full Postgres database, a user authentication system, and S3-compatible file storage behind a single API. You interact with it through the JavaScript SDK, the REST API, or directly via a Postgres connection with your ORM of choice.

Every request is scoped to a project. Row-level security (RLS) policies you define on your tables are enforced automatically on every query, whether it comes from the SDK, the REST API, or a direct database connection.

Projects & Organizations

Your account can have multiple organizations (teams or companies). Each organization contains one or more projects. A project is an isolated environment with its own database schema, API keys, auth users, and storage bucket.

ConceptWhat it isExample
OrganizationA team or company that owns projects"Acme Corp"
ProjectAn isolated backend environment"production", "staging"
API KeyCredentials scoped to a single projectlb_... (anon or service_role)

Projects are fully isolated. Two projects can have tables with the same name, different RLS policies, and separate auth user pools without any conflict.

API Keys

Each project has two types of API keys:

Key typeRLSUse case
anonEnforcedClient-side code, browser apps, mobile apps
service_roleBypassedServer-side code, admin scripts, migrations

The service_role key bypasses all row-level security. Never expose it in client-side code or commit it to version control.

Writing RLS policies

Policies can target a role with a TO clause, and Linabase maps the caller onto one of two Postgres roles for every request:

CallerRuns asMatches
anon keyanonTO anon, TO public
signed-in user JWTauthenticatedTO authenticated, TO public
service_role key(bypasses RLS entirely)n/a
Both of these work
-- Role-targeted: only signed-in users, and only their own row
CREATE POLICY "update own profile" ON profiles
  FOR UPDATE TO authenticated
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Role-agnostic: applies to every caller, scoped by the predicate
CREATE POLICY "read published" ON posts
  FOR SELECT
  USING (published);

Prefer auth.uid() in the USING / WITH CHECK predicate over a TO clause for identifying a user. TO distinguishes anonymous from signed-in callers; only the predicate identifies which user.

A table with RLS enabled and no policy matching the caller returns zero rows with a 200, not an error. An empty result is indistinguishable from a working filter, so if a query returns nothing unexpectedly, check that a policy applies to that caller and that command before assuming the data is missing.

Schema Isolation

Each project gets four Postgres schemas that keep your data, auth, storage, and logs separate:

You seeActual schemaContents
publicproj_{id}Your tables, views, and functions
authproj_{id}_authAuth users, sessions, identities (read-only)
storageproj_{id}_storageBuckets and file metadata (read-only)
logsproj_{id}_logsAudit and email logs (read-only)

When you query from("users") in the SDK or /rest/v1/users in the REST API, it resolves to proj_{id}.users automatically. You never need to reference the internal schema name.

Quick Start

Get a working query running in under 2 minutes. You need a Linabase account and a project with at least one table.

1. Install the SDK

Terminal
npm install @linabase/js

2. Initialize the client

Get your project URL and anon key from the API Keys page in your dashboard.

src/lib/linabase.ts
import { createClient } from "@linabase/js";

const linabase = createClient({
  url: "https://linabase.com",
  anonKey: "lb_your_anon_key_here",
});

export default linabase;

3. Run your first query

The SDK returns { data, error } from every operation. Always check error before using data.

src/app.ts
import linabase from "./lib/linabase";

// Fetch rows (RLS is enforced automatically)
const { data, error } = await linabase
  .from("posts")
  .select("id, title, author:profiles(name)")
  .order("created_at", { ascending: false })
  .limit(10);

if (error) {
  console.error("Query failed:", error.message);
} else {
  console.log("Posts:", data);
}

The author:profiles(name) syntax joins the profiles table via a foreign key and returns the name column as author. This works for any FK relationship in your schema.

Ready to try it?

Create a free account and get your API keys in seconds.

Get started free

JavaScript SDK

The @linabase/js SDK provides a Supabase-compatible client for database queries, authentication, and file storage. If you have used the Supabase JS client, the API is nearly identical.

The SDK automatically manages access tokens. After a successful sign-in, every subsequent database and storage request includes the user's JWT. You do not need to pass tokens manually.

See the full auto-generated SDK reference →

Authentication

Linabase provides a per-project auth engine. Each project has its own user pool, so a user signed up in one project does not exist in another.

Sign up and sign in

When a user signs up, Linabase creates an auth record and returns a session with an access token. The SDK stores this token and attaches it to all future requests.

Sign up
const { data, error } = await linabase.auth.signUp({
  email: "user@example.com",
  password: "securepassword",
  data: { full_name: "Jane Doe" }, // optional metadata
});
Sign in
const { data, error } = await linabase.auth.signIn({
  email: "user@example.com",
  password: "securepassword",
});
// data.session.access_token is set automatically for subsequent requests

OAuth providers

Redirect users to a third-party provider. After authentication, they are redirected back to your app with a valid session.

OAuth
await linabase.auth.signInWithOAuth({
  provider: "google", // or github, apple, discord, etc.
  redirectTo: "https://yourapp.com/callback",
});

Session management

Check the current session, listen for auth state changes, or sign the user out.

Session
// Get current session
const { data: session } = await linabase.auth.getSession();

// Get current user
const { data: user } = await linabase.auth.getUser();

// Listen for auth changes
const { unsubscribe } = linabase.auth.onAuthStateChange((event, session) => {
  console.log(event); // "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED"
});

// Sign out
await linabase.auth.signOut();

Password reset

Send a reset email, then update the password after the user clicks the link. The SDK handles the token exchange.

Password reset
// Request reset email
await linabase.auth.resetPasswordForEmail("user@example.com");

// Update password (after clicking reset link)
await linabase.auth.updatePassword("new_password");

Admin operations

Server-side code using a service_role key can manage users directly without requiring them to go through the sign-up flow.

Admin (service_role key)
const { data } = await linabase.auth.admin.listUsers({ page: 1, per_page: 50 });
const { data: user } = await linabase.auth.admin.createUser({
  email: "new@example.com",
  password: "password",
  email_confirm: true,
});
await linabase.auth.admin.deleteUser(user.id);

Admin methods require a service_role key. Never call these from client-side code.

Full AuthClient reference →

Database

Query your Postgres database with a chainable builder API. Every query goes through the REST API and respects row-level security. The user making the request only sees rows their RLS policies allow.

Reading data

Use .select() to fetch rows. You can filter, sort, paginate, and join related tables in a single call.

Select with filters
const { data } = await linabase
  .from("products")
  .select("id, name, price, category:categories(name)")
  .gte("price", 10)
  .lte("price", 100)
  .order("price", { ascending: true })
  .limit(20);

Writing data

Insert single rows or batches. Use .upsert() when you want to update an existing row or insert a new one based on a unique constraint.

Insert, update, delete
// Insert (single or batch)
await linabase.from("products").insert({ name: "Widget", price: 9.99 });
await linabase.from("products").insert([{ name: "A" }, { name: "B" }]);

// Upsert (insert or update on conflict)
await linabase.from("products").upsert({ id: 1, name: "Updated Widget" });

// Update with filter
await linabase.from("products").update({ price: 14.99 }).eq("id", 1);

// Delete with filter
await linabase.from("products").delete().eq("id", 1);

Filter reference

Chain any of these methods to narrow your query. They map directly to Postgres operators.

MethodSQL EquivalentExample
.eq(col, val)= val.eq("status", "active")
.neq(col, val)!= val.neq("role", "admin")
.gt(col, val)> val.gt("age", 18)
.gte(col, val)>= val.gte("price", 10)
.lt(col, val)< val.lt("stock", 5)
.lte(col, val)<= val.lte("rating", 3)
.like(col, pat)LIKE pat.like("name", "%widget%")
.ilike(col, pat)ILIKE pat.ilike("email", "%@gmail%")
.in(col, vals)IN (...).in("id", [1, 2, 3])
.is(col, val)IS val.is("deleted_at", "null")
.contains(col, val)@> val.contains("tags", ["new"])
.textSearch(col, q)to_tsquery.textSearch("body", "hello & world")
.not(col, op, val)NOT op.not("age", "lt", "18")
.or(filter)OR.or("age.gt.20,name.eq.John")

Calling Postgres functions

Execute server-side functions defined in your schema. Arguments are passed as a JSON object.

RPC
const { data } = await linabase.rpc("get_top_users", { limit: 10 });

TypeScript type generation

Generate types from your database schema so every query is type-checked at compile time.

Generate types
const types = await linabase.generateTypes();
// Write to src/types/database.ts for type-safe queries

Run type generation in CI or as a post-migration hook so your types stay in sync with your schema.

Full DatabaseClient reference →

Storage

Each project gets an S3-compatible storage bucket. You create logical "buckets" (like avatars or documents) to organize files. The public flag on a bucket decides how its objects are read. A public bucket is served at a permanent URL that needs no key at all, the same way Supabase serves one. A private bucket has no such URL: every read is a time-limited signed URL, mintable only with an authorized session or a service_role key.

Upload and download

Upload
const { data, error } = await linabase.storage
  .from("avatars")
  .upload("user-123/avatar.png", file, { contentType: "image/png" });
Download
const { data: blob } = await linabase.storage
  .from("avatars")
  .download("user-123/avatar.png");

Public and signed URLs

For a public bucket, getPublicUrl() builds a permanent URL offline: it makes no request and does not check that the object exists. It needs projectId on the client config, because a public URL carries no API key and so has to name the project itself. For a private bucket, createSignedUrl() mints a time-limited URL instead; asking again for the same object shortly after returns the still-valid URL rather than minting a new one.

URLs
// Permanent, no request, public buckets only
const { data: { publicUrl } } = linabase.storage
  .from("avatars")
  .getPublicUrl("user-123/avatar.png");

// Resized on the fly
linabase.storage.from("avatars").getPublicUrl("user-123/avatar.png", {
  transform: { width: 200, format: "webp" },
});

// Signed URL for a private bucket, expires in 1 hour
const { data: { signedUrl } } = await linabase.storage
  .from("documents")
  .createSignedUrl("report.pdf", 3600);

File management

List, delete, move, copy
// List files in a bucket
const { data: files } = await linabase.storage.from("avatars").list();

// Delete a file
await linabase.storage.from("avatars").remove("user-123/avatar.png");

// Move or copy
await linabase.storage.from("avatars").move("old.png", "new.png");
await linabase.storage.from("avatars").copy("orig.png", "backup.png");

Full StorageClient and BucketClient reference →

Storage access control

File metadata lives in real Postgres tables with row-level security on them, exactly like your own tables. Storage starts closed. A new project ships no policy for anon or authenticated, and RLS with no matching policy denies, so end users reach nothing until you write one. This is the same posture Supabase has: you open what you mean to open.

What works without any policy

Four paths are never gated by policies, so storage is usable before you write any SQL:

  • The service_role key bypasses RLS entirely, so server-side code keeps working.
  • Public buckets are served at their permanent URL with no key and no policy check. The bucket's public flag is the authorization.
  • Signed URLs are capabilities: the signature is the authorization, so a signed link works regardless of policies.
  • The dashboard always shows you your own files.

Policies decide one thing: what your end users reach with an anon key or a signed-in user's JWT.

Opening it up

One policy, on objects. Buckets are resolved for you before your policies are applied, the same way Supabase's storage service does it, so you do not need a policy on buckets to make files reachable.

The common case: each user owns their files
CREATE POLICY storage_own_files ON objects
  FOR ALL TO authenticated
  USING (owner_id = auth.uid()::text)
  WITH CHECK (owner_id = auth.uid()::text);

owner_id is set to the uploading user whenever a file arrives with a user JWT, so this gives every user a private area without needing a path convention. Run these in the SQL editor with your project selected: the storage tables resolve on the search path, so they need no schema prefix.

Folder per user, if you prefer paths
CREATE POLICY storage_own_folder ON objects
  FOR ALL TO authenticated
  USING (path LIKE auth.uid()::text || '/%')
  WITH CHECK (path LIKE auth.uid()::text || '/%');
Letting anonymous visitors read one bucket
CREATE POLICY storage_anon_reads ON objects
  FOR SELECT TO anon
  USING (bucket_id = (SELECT id FROM buckets WHERE name = 'avatars'));

Coming from Supabase

The model is the same, but the columns are not, so policies need small edits rather than pasting:

  • The object path column is path, not name.
  • bucket_id is a uuid, not the bucket name. Match it with (SELECT id FROM buckets WHERE name = 'avatars') instead of bucket_id = 'avatars'.
  • There is no storage.foldername(), filename() or extension(). Use LIKE on path, or split_part(path, '/', 1).

auth.uid(), auth.role(), auth.email() and auth.jwt() all work in storage policies exactly as they do elsewhere. auth.uid() returns NULL when the caller is not a project user, an anon key for instance, so an owner-scoped policy matches nothing for them rather than erroring.

Reading file metadata over REST

The storage tables are not reachable through the REST API with an anon key or a user JWT. List files with the SDK's list(), which applies your bucket rules. If you need the raw rows, query them over REST with a service_role key from server-side code.

Realtime (Event Webhooks)

Linabase fires webhooks on INSERT, UPDATE, and DELETE operations. Configure them in the dashboard under your project's webhook settings, or via the API.

Each webhook is signed with HMAC-SHA256 using a per-webhook secret. Verify the X-Webhook-Signature header on your server to confirm the payload came from Linabase.

Webhook payload
{
  "event": "insert",
  "table": "posts",
  "tenant_id": "abc123",
  "timestamp": "2026-04-04T12:00:00.000Z",
  "record": { "id": 1, "title": "New post" },
  "old_record": null
}

For UPDATE events, old_record contains the row before the change. For DELETE events, record is null and old_record contains the deleted row.

REST API

The REST API provides PostgREST-compatible endpoints for direct database access. Use it when you are not using the JavaScript SDK, for example from a Go, Python, or Ruby backend, or from curl scripts.

Every request requires an Authorization header with a Bearer token. The token can be an API key or a user JWT. The token type determines the caller's identity and whether RLS is enforced.

Using an API key (anonymous access)
# Pass your anon key as a Bearer token. RLS is enforced.
curl https://linabase.com/rest/v1/posts \
  -H "Authorization: Bearer lb_your_anon_key"
Using a user JWT (authenticated access)
# After sign-in, pass the user's JWT. RLS sees auth.uid() = the user's ID.
curl https://linabase.com/rest/v1/posts \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Using a service_role key (bypasses RLS)
# The service_role key bypasses all RLS. Use only from server-side code.
curl https://linabase.com/rest/v1/posts \
  -H "Authorization: Bearer lb_your_service_role_key"

The SDK handles token management automatically. After auth.signIn(), it attaches the user's JWT to every request. You only need to manage tokens manually when calling the REST API directly.

Auth Endpoints

These endpoints manage your project's end-user authentication. They are compatible with the GoTrue API format.

POST/auth/v1/signup
curl -X POST /auth/v1/signup \
  -H "Authorization: Bearer lb_your_anon_key" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "password123"}'
POST/auth/v1/token?grant_type=password
POST/auth/v1/token?grant_type=refresh_token
POST/auth/v1/logout
GET/auth/v1/user
PUT/auth/v1/user
POST/auth/v1/recover
POST/auth/v1/verify
GET/auth/v1/settings

CRUD Operations

Read, insert, update, and delete rows from any table in your project. Filters, sorting, and pagination are applied via query parameters.

GET/rest/v1/:table
# Select specific columns with filters
curl "/rest/v1/posts?select=id,title,author:profiles(name)&status=eq.published&order=created_at.desc&limit=10" \
  -H "Authorization: Bearer lb_your_anon_key"
POST/rest/v1/:table
curl -X POST /rest/v1/posts \
  -H "Authorization: Bearer lb_your_anon_key" \
  -H "Content-Type: application/json" \
  -H "Prefer: return=representation" \
  -d '{"title": "Hello", "body": "World"}'
PATCH/rest/v1/:table?filters
DELETE/rest/v1/:table?filters

Use Prefer: return=representation to get the modified rows back in the response. Use Prefer: resolution=merge-duplicates for upsert behavior on POST.

Filtering

Append filters as query parameters using the format ?column=operator.value. These map directly to Postgres operators.

OperatorDescriptionExample
eqEqual?status=eq.active
neqNot equal?role=neq.admin
gt / gteGreater than (or equal)?age=gt.18
lt / lteLess than (or equal)?price=lt.100
like / ilikePattern match?name=ilike.*widget*
inIn list?id=in.(1,2,3)
isIS (null/true/false)?deleted_at=is.null
cs / cdContains / contained by?tags=cs.{new,sale}
ovOverlaps?dates=ov.[2026-01-01,2026-12-31]
fts / plfts / phftsFull-text search?body=fts.hello+world
notNegate?age=not.lt.18
orOR conditions?or=(age.gt.20,name.eq.John)

RPC / Functions

Call Postgres functions you have defined in your schema. Arguments are passed as a JSON body. The function must be in the project's public schema.

POST/rest/v1/rpc/:function_name
curl -X POST /rest/v1/rpc/get_top_users \
  -H "Authorization: Bearer lb_your_anon_key" \
  -H "Content-Type: application/json" \
  -d '{"limit_count": 10}'

Schema Selection

By default, the REST API reads and writes to your project's public schema (your tables). To access other schemas, use the Accept-Profile header for reads and Content-Profile for writes.

SchemaDescriptionAccess
publicYour project tables (default)Read/Write — any key
authAuth users, sessions, identitiesRead-only — service_role only
storageBuckets and file metadataRead-only — any key
logsAudit logs, email logsRead-only — service_role only
# Read from auth schema (requires service_role key)
curl /rest/v1/auth_users \
  -H "Accept-Profile: auth" \
  -H "Authorization: Bearer lb_your_service_role_key"

The auth, storage, and logs schemas are read-only via the REST API. Writes to auth users go through the /auth/v1/* endpoints; file writes go through the storage API.

The auth and logs schemas require a service_role key and return 403 for an anon key. They hold credentials and audit history, so never query them from a browser or anywhere else your publishable key is visible. public and storage are reachable with an anon key.

Admin Endpoints

These endpoints require a service_role API key and allow you to manage users and inspect the API schema.

GET/auth/v1/admin/users
POST/auth/v1/admin/users
GET/auth/v1/admin/users/:id
PUT/auth/v1/admin/users/:id
DELETE/auth/v1/admin/users/:id
GET/rest/v1/openapi.json
GET/rest/v1/types

Use /rest/v1/openapi.json to auto-generate client libraries in any language, or import into Postman/Insomnia for interactive testing.

ORMs (Drizzle, Prisma, Knex)

Every Linabase project exposes a standard Postgres connection. You can use any ORM or database client that supports Postgres. This means you are never locked into the Linabase SDK; your existing codebase works as-is.

Get your connection string from Project Settings > Database Connection in the dashboard. It includes the host, port, database name, role, and password.

ORM connections go directly to Postgres, not through the REST API, and row-level security does not apply to them. The connection string uses your project’s owning role, and in Postgres a table’s owner bypasses its RLS policies. Treat this connection like a service_role key: server-side only, never shipped to a client. What it still cannot do is reach another project — the role only has access to your project’s own schemas.

Drizzle ORM

Install
npm install drizzle-orm pg

For runtime queries, point Drizzle at your connection string. Table names resolve automatically because the role's search_path is set to your project schema.

Runtime queries
import { drizzle } from "drizzle-orm/node-postgres";

// Connection string from Project Settings > Database Connection
const db = drizzle("postgresql://tenant_abc:password@host:3105/linabase");

// Queries work with unqualified table names
const users = await db.select().from(profiles);

For drizzle-kit (schema pull, push, migrations), you need to specify the schema filter so it only reads your project's schema, not system schemas.

drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "postgresql",
  dbCredentials: {
    url: "postgresql://tenant_abc:password@host:3105/linabase",
  },
  // Copy the schema name from Project Settings > Database Connection > Schema
  schemaFilter: ["proj_abc123def456"],
});
drizzle-kit commands
# Pull existing schema into Drizzle schema files
npx drizzle-kit pull

# Push schema changes to the database
npx drizzle-kit push

# Generate migration files
npx drizzle-kit generate

Prisma

Prisma requires the multiSchema preview feature to work with Linabase's schema-per-project architecture.

schema.prisma
datasource db {
  provider = "postgresql"
  url      = "postgresql://tenant_abc:password@host:3105/linabase"
  schemas  = ["proj_abc123def456"]
}

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["multiSchema"]
}
Prisma commands
# Pull existing schema
npx prisma db pull

# Push changes
npx prisma db push

# Generate client
npx prisma generate

Knex / raw pg

Knex and raw pg clients work without any schema configuration because the tenant role's search_path resolves unqualified table names automatically.

Knex
import knex from "knex";

const db = knex({
  client: "pg",
  connection: "postgresql://tenant_abc:password@host:3105/linabase",
});

const posts = await db("posts").select("*").limit(10);

MCP Server

The Linabase MCP server lets AI coding assistants (Claude Code, Cursor, Windsurf) interact with your project directly. It provides 28 tools for schema exploration, CRUD operations, DDL, RLS management, storage, auth user management, TypeScript generation, and performance advice.

OAuth connection (recommended)

Add this to your AI client's MCP configuration. It opens a browser window for login, no API keys needed.

MCP config (OAuth)
{
  "mcpServers": {
    "linabase": {
      "type": "http",
      "url": "https://linabase.com/mcp"
    }
  }
}

With no headers, the consent screen shows a project picker the first time you connect. The OAuth token your client stores is then bound to that project for its lifetime.

Pin a project per repository

To make a repository always connect to a specific Linabase project, add X-Linabase-Project in headers. The consent screen will skip the picker and confirm authorization for that one project.

MCP config (project pinned)
{
  "mcpServers": {
    "linabase": {
      "type": "http",
      "url": "https://linabase.com/mcp",
      "headers": {
        "X-Linabase-Project": "hooklink"
      }
    }
  }
}

If the same project slug exists in more than one organization you belong to, the consent screen returns an error asking you to disambiguate. Add X-Linabase-Org for that case.

MCP config (org + project pinned)
{
  "mcpServers": {
    "linabase": {
      "type": "http",
      "url": "https://linabase.com/mcp",
      "headers": {
        "X-Linabase-Org":     "my-org",
        "X-Linabase-Project": "hooklink"
      }
    }
  }
}

Header values are slug-only: lowercase letters, digits, and hyphens (max 63 chars). If your token was issued for a different project, the server rejects the call with 403 token_project_mismatch until you re-authorize. Headers are optional; existing configs without them keep working.

Self-Hosting Coming soon

Self-hosted Linabase is on the roadmap but not available yet. The current platform is a multi-tenant cloud service. A single-tenant build you can run on your own infrastructure is planned for a future release.

Until then, the only way to use Linabase is the hosted cloud at linabase.com. If self-hosting is important to you, follow the project for updates.

Pricing & Usage

Linabase uses pay-for-what-you-use pricing. There are no tiers and no feature gates. Every feature is available to every account.

Free Tier

Every organization gets 500 MB of database storage and 1 GB of file storage, free, with no credit card. Adding a payment method raises those limits, and you are billed for what you store above the free tier.

Billing formula
bill = storage above the free tier, metered per GB

Stay inside the free tier and you pay nothing, which is enough to develop against and to run a small app. Writes stop at the free ceiling until a payment method is added, so you are never billed by surprise.

Usage Metering

Three resources are metered. Usage is pooled across all projects in your organization.

ResourcePriceWhat counts
Database storage$0.25/GB/monthTotal data across all project schemas (tables, indexes, auth users)
File storage$0.10/GB/monthFiles stored in all project buckets
Bandwidth$0.01/GBAPI responses and file downloads (uploads are free)

Email sends and database connections are not billed. Backups do not count against your storage. The billing page shows your current cost, credit applied, and estimated bill in real time.

What Happens When You Exceed the Credit

When your monthly usage exceeds $10 and you have no payment method on file:

OperationStatus
Read operations (SELECT, GET, downloads)Allowed
Delete operations (helps you free space)Allowed
Write operations (INSERT, UPDATE, uploads)Blocked (402)

Add a payment method to resume write access. You will only be charged for usage above the $10 credit at the end of each billing cycle.

Self-Hosted Coming soon

A self-hosted build is on the roadmap. When it ships, you will be able to run Linabase on your own hardware with no usage caps and no telemetry. It is not available today.

Next steps

  • Create a free account to get your API keys
  • Install the SDK with npm install @linabase/js
  • Follow the Quick Start above to run your first query