# Claude Code plugin Source: https://docs.larksh.com/cli/claude-code Use Lark with Claude Code for AI-assisted development # Claude Code plugin The official Lark plugin for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) gives Claude full context on the Lark CLI, REST API, admin API, and common development patterns. When the plugin is active, Claude can help you write Lark integration code, debug issues, manage your databases, and set up CI/CD pipelines, all with accurate, up-to-date knowledge of the Lark platform. ## What it includes * Full documentation for `lark data`, `lark projects`, `lark databases`, `lark rules`, and all other CLI commands. * REST data API and admin API endpoints, authentication details, and request/response formats. * Common patterns like backup and restore, data seeding, watch scripts, and CI/CD integration. * ID formats, naming standards, and conventions for structuring your Lark data. ## Installation Install the plugin from the Lark marketplace: ```bash theme={null} /plugin marketplace add lark-sh/lark-skill /plugin install lark@lark-tools ``` ## Usage Use `/lark` in any Claude Code conversation to activate the plugin manually. Claude will also load it automatically when it detects you're working with Lark. Once active, you can ask Claude things like: * "Set up a Lark database for my multiplayer game" * "Write a backup script that exports my database nightly" * "Help me write security rules for user-owned data" * "Create a CI pipeline that deploys my security rules on push" # Command reference Source: https://docs.larksh.com/cli/commands Complete reference for every Lark CLI command # Command reference All commands support the `--json` flag for machine-readable output. ## Authentication ### `lark login` Log in to Lark via your browser. Opens the dashboard authorization page, and the CLI receives your session token automatically. ```bash theme={null} lark login ``` Your session is saved to `~/.lark/config.json`. ### `lark logout` Log out and clear the stored session. ```bash theme={null} lark logout ``` ### `lark whoami` Show the currently logged-in user. ```bash theme={null} lark whoami # Riley Dutton (riley@example.com) ``` ## Configuration ### `lark config set-project ` Set the default project for all commands. The project is verified before saving. ```bash theme={null} lark config set-project my-game ``` ### `lark config show` Show current configuration. ```bash theme={null} lark config show # Default project: my-game ``` ## Projects ### `lark projects list` List all your projects. ```bash theme={null} lark projects list ``` ### `lark projects create ` Create a new project. The name is converted to a DNS-compatible project ID automatically. ```bash theme={null} lark projects create "My Game" # Created project: my-game ``` ### `lark projects show [id]` Show project details including settings, secret key, and database counts. Uses the default project if `id` is omitted. ```bash theme={null} lark projects show ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark projects update [id]` Update project settings. At least one setting flag is required. ```bash theme={null} lark projects update --auto-create true --ephemeral false ``` | Flag | Description | | ---------------------------- | ---------------------------------------------- | | `--name ` | Project name. | | `--ephemeral ` | Enable/disable ephemeral mode. | | `--auto-create ` | Enable/disable auto-create databases. | | `--firebase-project-id ` | Firebase project ID for auth token validation. | | `--project ` | Override the default project. | ### `lark projects delete [id]` Delete a project and all its databases. This is irreversible. ```bash theme={null} lark projects delete --confirm my-game ``` | Flag | Description | | ---------------- | ------------------------------------------------------ | | `--confirm ` | **Required.** Pass the project ID to confirm deletion. | | `--project ` | Override the default project. | ### `lark projects regenerate-secret [id]` Regenerate the project secret key. The old key is immediately invalidated. ```bash theme={null} lark projects regenerate-secret ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ## Databases `lark databases` can be shortened to `lark db`. ### `lark db list` List databases in the current project. ```bash theme={null} lark db list lark db list --search game --limit 10 ``` | Flag | Description | | ------------------ | --------------------------------------- | | `--search ` | Filter databases by ID. | | `--limit ` | Max results (default: 50). | | `--offset ` | Number of results to skip (default: 0). | | `--project ` | Override the default project. | ### `lark db create ` Create a new database. ```bash theme={null} lark db create game-state ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark db delete ` Delete a database and all its data. This is irreversible. ```bash theme={null} lark db delete game-state ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ## Data All data commands require a database ID and most require a path. Paths start with `/`. ### `lark data get ` Read data at a path. ```bash theme={null} lark data get game-state /players/alice # {"name": "Alice", "score": 42} ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark data set ` Overwrite data at a path. Use `-` as the value to read from stdin. ```bash theme={null} lark data set game-state /players/alice '{"name": "Alice", "score": 0}' # From stdin cat alice.json | lark data set game-state /players/alice - ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark data update ` Shallow merge data at a path. Existing keys not in the payload are preserved. Use `-` for stdin. ```bash theme={null} lark data update game-state /players/alice '{"score": 99}' ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark data push ` Push data with an auto-generated key. Returns the generated key. Use `-` for stdin. ```bash theme={null} lark data push game-state /messages '{"text": "Hello!", "sender": "alice"}' # -Nabc123def ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark data delete ` Delete data at a path. ```bash theme={null} lark data delete game-state /players/alice ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark data export [path]` Export data as JSON. Path defaults to `/` (entire database). ```bash theme={null} # Export entire database to a file lark data export game-state -o backup.json # Export a subtree to stdout lark data export game-state /players ``` | Flag | Description | | --------------------- | ---------------------------------- | | `-o, --output ` | Write to a file instead of stdout. | | `--project ` | Override the default project. | ### `lark data import [path]` Import data from a JSON file. Path defaults to `/`. This overwrites data at the target path. ```bash theme={null} lark data import game-state -f backup.json lark data import game-state /players -f players.json ``` | Flag | Description | | ------------------- | ---------------------------------- | | `-f, --file ` | **Required.** JSON file to import. | | `--project ` | Override the default project. | Import overwrites all data at the target path. If you import to `/`, the entire database is replaced. ### `lark data watch ` Stream real-time changes as Server-Sent Events. Press `Ctrl+C` to stop. ```bash theme={null} lark data watch game-state /players ``` Output: ``` [put] {"path":"/","data":{"alice":{"name":"Alice","score":42}}} [patch] {"path":"/alice/score","data":99} ``` With `--json`, outputs newline-delimited JSON (one object per line): ```bash theme={null} lark data watch game-state /players --json ``` ```json theme={null} {"event":"put","data":{"path":"/","data":{"alice":{"name":"Alice","score":42}}}} {"event":"patch","data":{"path":"/alice/score","data":99}} ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ## Security rules ### `lark rules get [id]` Get the current security rules for a project. Uses the default project if `id` is omitted. ```bash theme={null} lark rules get ``` | Flag | Description | | ---------------- | ----------------------------- | | `--project ` | Override the default project. | ### `lark rules set [id]` Set security rules from a file or stdin. Supports JSON5 (comments and trailing commas). Uses the default project if `id` is omitted. ```bash theme={null} # From a file lark rules set -f rules.json # From stdin cat rules.json | lark rules set ``` | Flag | Description | | ------------------- | ----------------------------- | | `-f, --file ` | Rules file to upload. | | `--project ` | Override the default project. | ## Monitoring ### `lark dashboard [id]` Show project metrics. Uses the default project if `id` is omitted. Defaults to the last 24 hours. ```bash theme={null} lark dashboard lark dashboard --start 2026-02-01 --end 2026-02-15 ``` Displays: * **Current**: CCU, peak CCU, bytes in/out, reads/writes, permission denials * **Summary**: Peak CCU, total bandwidth, total operations, average latency * **Recent events**: Last 10 events | Flag | Description | | ---------------- | ----------------------------- | | `--start ` | Start date (ISO format). | | `--end ` | End date (ISO format). | | `--project ` | Override the default project. | ### `lark events [id]` Show project events (connections, errors, warnings). Uses the default project if `id` is omitted. ```bash theme={null} lark events lark events --limit 50 ``` | Flag | Description | | ---------------- | --------------------------------------- | | `--limit ` | Max results (default: 25). | | `--offset ` | Number of results to skip (default: 0). | | `--project ` | Override the default project. | ### `lark billing [id]` Show billing information. Uses the default project if `id` is omitted. Defaults to the current billing period. ```bash theme={null} lark billing lark billing --period 2026-01 ``` Displays: period start, peak CCU, total bandwidth, total storage. | Flag | Description | | ------------------ | ----------------------------------- | | `--period ` | Billing period in `YYYY-MM` format. | | `--project ` | Override the default project. | ## Global flags These flags work with every command: | Flag | Description | | ---------------- | ---------------------------------------------- | | `--json` | Output results as machine-readable JSON. | | `--project ` | Override the default project for this command. | ## Configuration file The CLI stores its configuration in `~/.lark/config.json`. This file is created automatically when you run `lark login` and contains your session token and default project setting. ```bash theme={null} # View your config lark config show # Set default project lark config set-project my-game ``` You generally don't need to edit this file directly. # CLI overview Source: https://docs.larksh.com/cli/overview Manage your Lark projects, databases, and data from the terminal # Lark CLI The Lark CLI (`@lark-sh/cli`) gives you full control over your projects, databases, security rules, and data from the terminal. Anything you can do in the dashboard, you can do from the command line, plus data operations like export, import, and real-time streaming. ## Installation ```bash theme={null} npm install -g @lark-sh/cli ``` Or run commands directly without installing: ```bash theme={null} npx @lark-sh/cli ``` ## Getting started ### Log in ```bash theme={null} lark login ``` This opens your browser to the Lark dashboard. Authorize the CLI, and you're in. Your session is stored in `~/.lark/config.json`. ### Set a default project Most commands need a project ID. Set a default so you don't have to pass `--project` every time: ```bash theme={null} lark config set-project my-project ``` You can always override it per-command with `--project `. ## Common workflows ### Create a project and database ```bash theme={null} # Create a new project lark projects create "My Game" # Created project: my-game # Set it as default lark config set-project my-game # Create a database lark db create game-state ``` ### Read and write data ```bash theme={null} # Write data lark data set game-state /players/alice '{"name": "Alice", "score": 0}' # Read it back lark data get game-state /players/alice # {"name": "Alice", "score": 0} # Update a single field (shallow merge) lark data update game-state /players/alice '{"score": 42}' # Push to a list with an auto-generated key lark data push game-state /messages '{"text": "Hello!", "sender": "alice"}' # -Nabc123def # Delete data lark data delete game-state /players/alice ``` ### Pipe data from stdin Use `-` as the value to read from stdin. This is useful for large payloads or scripting: ```bash theme={null} cat player-data.json | lark data set game-state /players - echo '{"score": 99}' | lark data update game-state /players/alice - ``` ### Watch for real-time changes Stream live changes to your terminal: ```bash theme={null} lark data watch game-state /players ``` Events print as they happen: ``` [put] {"path":"/","data":{"alice":{"name":"Alice","score":42}}} [patch] {"path":"/alice/score","data":99} ``` Press `Ctrl+C` to stop. ### Export and import data ```bash theme={null} # Export a database to a file lark data export game-state -o backup.json # Export a specific path lark data export game-state /players -o players.json # Import data from a file lark data import game-state -f backup.json # Import to a specific path lark data import game-state /players -f players.json ``` ### Manage security rules ```bash theme={null} # View current rules lark rules get # Set rules from a file (JSON5 supported) lark rules set -f rules.json # Or pipe them in cat rules.json | lark rules set ``` ### Check metrics ```bash theme={null} # Show dashboard with current metrics lark dashboard # View recent events lark events # Check billing for a specific month lark billing --period 2026-02 ``` ## JSON output for scripting Every command supports `--json` for machine-readable output: ```bash theme={null} # Get project details as JSON lark projects show --json # List databases as JSON lark db list --json # Use with jq lark data get game-state /players --json | jq '.alice.score' ``` ## What's next Full reference for every CLI command, flag, and option. The HTTP API that powers the CLI's data operations. # Database management Source: https://docs.larksh.com/dashboard/databases Create, browse, edit, and manage your databases The **Databases** section on your project dashboard lists all databases in your project. ## Listing databases Each database shows its ID, status (active or inactive), server ID, and last activity timestamp. Use the search box to filter by database ID. Results paginate at 100 per page. ## Creating a database Click **New Database** and enter an ID. Database IDs must be alphanumeric and no longer than 40 characters. Enable **Auto Create** in your [project settings](/dashboard/settings) to create databases automatically on first client connection. This is the easiest way to get started. ## Deleting a database Click the delete button next to any database. This is permanent. All data in that database is removed immediately. ## The data editor Click any database ID to open the real-time JSON editor. This gives you a live view of the data in your database. ### Tree view Used for smaller datasets. You get an interactive JSON tree where you can: * Expand and collapse nodes * Click any value to edit it inline * Add new properties * Delete nodes Changes are highlighted in amber and save immediately. ### Shallow view Automatically used for large datasets (more than 500 keys or 20MB). This shows a flat key list with search and sort. Click any key to navigate deeper into the tree. ### Navigation A breadcrumb trail at the top shows your current path. Click any segment to navigate back up. ### Connection status A badge in the editor shows whether you are connected to the live database. ## Import and export You can move data in and out of any database path. * Click **Export** to download the data at your current path as a `.json` file. Supports exports up to 256MB. * Click **Import** to upload a `.json` file that replaces data at the current path. Supports imports up to 256MB. Import replaces all data at the current path. It does not merge with existing data. # Monitoring Source: https://docs.larksh.com/dashboard/monitoring Track performance and usage from your project dashboard Your project dashboard shows real-time metrics and usage data so you can keep tabs on how your project is performing. ## Statistics cards These cards show a snapshot of the last 24 hours: * Peak CCU: peak and current concurrent users. * Bandwidth: total bytes in and out, with a breakdown. * Operations: total writes, reads, and events sent. * Processing: average latency in milliseconds. ## Charts Interactive line charts let you drill into trends. Use the time range selector to switch between **1 hour**, **24 hours**, and **7 days**. Available charts: * CCU over time * Bandwidth (bytes in and out) * Operations (writes, reads, events) * Latency (p50) ## Events log A log of recent system events from the last 7 days. You will see entries like: * High latency warnings * Connection rejected (at capacity) * Storage warnings # Dashboard overview Source: https://docs.larksh.com/dashboard/overview Manage your projects, databases, and settings from the Lark dashboard The Lark dashboard is where you manage everything. Projects, databases, security rules, monitoring: it all lives at [dashboard.lark.sh](https://dashboard.lark.sh). ## Signing in Sign in with Google OAuth at [dashboard.lark.sh](https://dashboard.lark.sh). Once authenticated, you land on your projects list. ## Projects list After signing in, you see all your projects. Each project shows: * Project name * Project ID * Database count (total and active) * Creation date ## Creating a project Click the **New Project** button to get started. Enter a name (max 20 characters), and Lark automatically generates a DNS-compatible project ID. This project ID is what you use in your SDK connection string to connect clients to your project. Choose a clear, descriptive project name. You can change it later, but the project ID is permanent. ## What's next Configure Firebase compatibility, authentication, and secret keys. Create databases, browse data, and import/export JSON. Write and publish security rules from the dashboard. Track connections, bandwidth, operations, and latency. # Pricing Source: https://docs.larksh.com/dashboard/pricing Lark pricing plans and billing Pricing details are coming soon. Lark is currently in early alpha. During this period, usage is free. Have questions about pricing or want early access? Reach out at [team@lark.sh](mailto:team@lark.sh). # Security rules editor Source: https://docs.larksh.com/dashboard/rules-editor Edit and deploy your security rules from the dashboard Security rules are edited in your project's **Settings** page, in the right-hand panel. ## Writing rules The editor supports JSON5 syntax, so comments, trailing commas, and unquoted keys are all valid. Write your rules, then click **Save Rules**. The dashboard validates your rules before saving. If there is a syntax error, you will see it immediately. Rules take effect as soon as they are saved. There is no separate deploy step. Start with permissive rules while developing, then tighten them before going to production. ## Example Here is a basic rule set that allows anyone to read player data, but only lets players write their own data: ```json theme={null} { "rules": { "players": { "$playerId": { ".read": true, ".write": "auth.uid === $playerId" } } } } ``` ## Learn more For the complete rules language reference (including path variables, built-in variables, and expression syntax), see [Security rules](/platform/security-rules). # Project settings Source: https://docs.larksh.com/dashboard/settings Configure your project's behavior, authentication, and security Access your project settings by clicking **Settings** from your project dashboard. ## Project name Your display name. You can edit it anytime. It has no effect on connections or configuration. ## Project ID Read-only after creation. This is the identifier used in SDK connection strings. Click the copy button next to it to grab it. ## Ephemeral mode When enabled, databases are automatically deleted when no active connections are detected. This is useful for temporary game sessions, testing environments, or any scenario where data doesn't need to persist. ## Auto Create When enabled, databases are created automatically the first time a client connects. When disabled, databases must be created manually in the dashboard before clients can connect. Keep **Auto Create** enabled while getting started. It removes friction during development. ## Firebase compatibility * **Firebase Auth Project ID**: Your Firebase project ID, used to validate Firebase Auth tokens. Set this if your users authenticate through Firebase Auth. ## Secret key Your secret key is used to sign Lark authentication tokens (HS256 JWTs). From this section you can view, copy, and regenerate your key. Keep your secret key on your server. Never expose it in client-side code. Regenerating your secret key invalidates all existing tokens immediately. ## Danger zone Delete the project permanently. You must type the project ID to confirm. This action cannot be undone. # Android SDK Source: https://docs.larksh.com/firebase/android Use the Firebase Android SDK with Lark Point your Firebase Android SDK at Lark by passing your Lark URL to `FirebaseDatabase.getInstance()`. Everything else (listeners, writes, queries) works without changes. ## Setup Add the Realtime Database dependency to your module's `build.gradle`: ```groovy theme={null} dependencies { implementation 'com.google.firebase:firebase-database' } ``` ## Connect to Lark Pass your Lark URL when getting the database instance: ```kotlin Kotlin theme={null} import com.google.firebase.database.FirebaseDatabase // Change this from https://PROJECT_ID.firebaseio.com val database = FirebaseDatabase.getInstance("https://your-lark-project-id.larkdb.net") val myRef = database.getReference("players/alice") ``` ```java Java theme={null} import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.database.DatabaseReference; // Change this from https://PROJECT_ID.firebaseio.com FirebaseDatabase database = FirebaseDatabase.getInstance("https://your-lark-project-id.larkdb.net"); DatabaseReference myRef = database.getReference("players/alice"); ``` If your `google-services.json` already has the `databaseURL` field, you can update it there instead and use `FirebaseDatabase.getInstance()` without arguments. ## Database routing If your Firebase app stores many independent data silos under path prefixes (game rooms, workspaces, etc.), see [Database routing](/firebase/database-routing) for options on splitting them into separate Lark databases. # Firebase Auth with Lark Source: https://docs.larksh.com/firebase/auth Use Firebase Authentication with your Lark database If your users authenticate through Firebase Auth, Lark can validate those tokens directly. No code changes needed on the client. ## Setup In your Lark project settings, set the **Firebase Auth Project ID** to your Firebase project ID. You can find this in the Firebase console under Project Settings. This tells Lark how to validate the Firebase Auth tokens your clients send. ## How it works 1. Your client authenticates with Firebase Auth as usual. 2. The Firebase SDK sends the user's ID token when connecting to the database. 3. Lark validates the token's signature using Firebase's public keys. 4. Once validated, the `auth` object in your security rules contains the user's `uid` and claims from the Firebase token. Your existing authentication flow works unchanged: ```javascript theme={null} // Your existing Firebase Auth code works as-is const user = firebase.auth().currentUser; const token = await user.getIdToken(); // The Firebase SDK automatically sends this token to Lark // when it connects to the database. You don't need to do // anything extra. ``` Lark validates Firebase Auth tokens automatically using Firebase's public keys. Once validated, the user's identity is available in your security rules just like it would be in Firebase. ## Security rules Once a Firebase Auth token is validated, the `auth` variable in your security rules works exactly as it does in Firebase: ```json theme={null} { "rules": { "users": { "$uid": { ".read": "auth.uid === $uid", ".write": "auth.uid === $uid" } } } } ``` Custom claims from Firebase Auth tokens are available under `auth.token`. For example, if you set an `admin` claim, you can check `auth.token.admin === true` in your rules. ## Using Lark's own auth instead You can also use Lark's own token system (HS256 JWTs signed with your secret key) alongside or instead of Firebase Auth. This is useful if you're migrating away from Firebase Auth entirely or building new features that don't depend on it. See [Authentication](/platform/authentication) for details on Lark's native auth system. # Compatibility notes Source: https://docs.larksh.com/firebase/compatibility What's supported, what's different, and known limitations Lark implements the Firebase Realtime Database wire protocol. Here's what works, what's different, and what to watch out for. ## Fully supported All core Firebase RTDB operations work with Lark: * Reads and writes: `set`, `update`, `remove`, `push`, `once` * Subscriptions: `on` / `off` with all event types (`value`, `child_added`, `child_changed`, `child_removed`, `child_moved`) * Queries: `orderByChild`, `orderByKey`, `orderByValue`, `orderByPriority`, `limitToFirst`, `limitToLast`, `startAt`, `endAt`, `equalTo` * Transactions with retries * onDisconnect: `onDisconnect().set()`, `onDisconnect().update()`, `onDisconnect().remove()`, `onDisconnect().cancel()` * Server values: `ServerValue.TIMESTAMP` * Priority: setting and querying by priority * REST API: `GET`, `PUT`, `POST`, `PATCH`, `DELETE`, query parameters, ETags, and Server-Sent Events streaming. See the [REST API documentation](/rest-api/overview) for full details. ## Wire protocol Lark implements the Firebase RTDB wire protocol. The Firebase SDK connects over WebSocket and communicates using the same JSON message format. From the SDK's perspective, Lark is indistinguishable from Firebase's servers. ## Security rules Same syntax, same semantics. Your existing Firebase security rules work in Lark without modification. The `auth` object, `$variables`, `.read`, `.write`, `.validate`, `.indexOn`, `newData`, `data`, `root`: all of it works identically. ## What's different A few things work differently in Lark: ### Connection URL You connect to `your-project-id.larkdb.net` instead of `your-project.firebaseio.com`. This is the only required code change. ### No Firebase CLI integration `firebase deploy --only database:rules` won't work with Lark. You can edit rules in the [Lark dashboard](/dashboard/rules-editor), or use the [Lark CLI](/cli/overview) to manage rules from the command line (`lark rules set -f rules.json`). ### No `.info/serverTimeOffset` Lark does not support the `.info/serverTimeOffset` path. If you need server time offset, use the [Lark SDK](/lark-sdk/overview) which provides this natively. The `.info/connected` path works as expected. You can still listen for connection state changes. ## Additional Lark features Even when using Firebase SDKs, you benefit from Lark's server-side features automatically: * Volatile path batching: updates to volatile paths are batched for efficiency. * Shared view optimization: when multiple clients subscribe to the same path, Lark optimizes the server-side work. * Lower latency: sub-50ms p99 latency for reads and writes. To use Lark's client-side features like WebTransport or the native SDK API, switch to the [Lark SDK](/lark-sdk/overview). You can use Firebase SDKs and the Lark SDK in the same project. Some clients can connect with Firebase SDKs while others use the Lark SDK. They all talk to the same database. # C++ SDK Source: https://docs.larksh.com/firebase/cpp Use the Firebase C++ SDK with Lark Point your Firebase C++ SDK at Lark by passing your Lark URL to `Database::GetInstance()`. Everything else (listeners, writes, queries) works without changes. ## Setup Add the Firebase C++ SDK to your project. Follow the [Firebase C++ setup guide](https://firebase.google.com/docs/cpp/setup) for your platform (Android NDK, iOS, or desktop). Make sure the Realtime Database library is linked: * **Android**: `firebase_database` in your `CMakeLists.txt` * **iOS**: `firebase.framework` and `firebase_database.framework` * **Desktop**: `firebase_database` library ## Connect to Lark Pass your Lark URL when getting the database instance: ```cpp theme={null} #include "firebase/app.h" #include "firebase/database.h" firebase::App* app = firebase::App::Create(); // Change this from https://PROJECT_ID.firebaseio.com firebase::database::Database* database = firebase::database::Database::GetInstance( app, "https://your-lark-project-id.larkdb.net"); firebase::database::DatabaseReference ref = database->GetReference("players/alice"); ``` If your `google-services.json` (Android) or `GoogleService-Info.plist` (iOS) already has the database URL, you can update it there instead and use `Database::GetInstance(app)` without a URL argument. ## Database routing If your Firebase app stores many independent data silos under path prefixes (game rooms, workspaces, etc.), see [Database routing](/firebase/database-routing) for options on splitting them into separate Lark databases. # Database routing Source: https://docs.larksh.com/firebase/database-routing One database by default, or many when you need them Every Lark project starts with a single database called `default`. When your connection URL points at the project and nothing else, that's where your data goes. For most apps that's all you ever need, and it's how Firebase Realtime Database already works: one project, one database, one JSON tree. ## The default approach Point your `databaseURL` at your project and you're talking to the `default` database: ```javascript theme={null} const config = { databaseURL: 'https://your-project.larkdb.net' }; const db = firebase.initializeApp(config).database(); db.ref('room-abc123/players/alice').set({ name: 'Alice' }); ``` There's no database name in that URL, so Lark uses `default`. `https://your-project.larkdb.net` is identical to `https://default--your-project.larkdb.net`; the first is just shorthand for the second. If you're migrating from Firebase, this is the whole story. Change `your-project.firebaseio.com` to `your-project.larkdb.net`, keep every path, rule, and query exactly as they were, and your data lands in one database the same way it did before. See [Migrating from Firebase](/firebase/migration) for the full walkthrough. A single database handles a lot. If your app has one logical dataset, a moderate number of rooms, or write rates that one tree can absorb, leave it here. ## Multiple databases Firebase Realtime Database caps you at a handful of database instances, so apps with many independent data silos (game rooms, workspaces, documents, sessions) end up packing them all into one database under path prefixes: ``` your-project.firebaseio.com/ room-abc123/ players/... chat/... room-def456/ players/... chat/... ... (thousands more) ``` Every room then shares one database, one set of write locks, and one security rules tree. At scale, that contention becomes a bottleneck. Lark removes the cap. With **Auto Create** enabled in your project settings, a database springs into existence the first time a client connects to it, so each room, workspace, or document can live in its own isolated database. You select the database by putting its name in the subdomain, ahead of your project ID and separated by `--`: ``` https://room-abc123--your-project.larkdb.net ``` The `room-abc123` before the `--` is the database name. Lark routes this connection to a database called `room-abc123` inside your project. Your client code keeps writing to the same paths: ```javascript theme={null} const roomId = 'room-abc123'; const config = { databaseURL: `https://${roomId}--your-project.larkdb.net` }; const db = firebase.initializeApp(config).database(); // Same paths as before, the room prefix is still there db.ref(`${roomId}/players/alice`).set({ name: 'Alice' }); ``` That makes the first path segment (`room-abc123`) redundant, since it matches the database name. Leaving it in costs you nothing and keeps your paths, security rules, and data model identical to what you ran on Firebase. The only thing that changes is the connection URL, usually one line where you build the config. Inside the `room-abc123` database, the data looks like: ``` room-abc123/ players/ alice/... chat/... ``` Each room gets its own write locks and its own rules evaluation, with no cross-room contention. For apps with thousands of concurrent rooms and high write rates, that isolation is what keeps writes fast as you grow. ## Which one to use Stay on the default database when one tree can hold everything comfortably. Move to multiple databases when you have many independent silos that would otherwise fight over the same locks, or when you want each tenant's data fully isolated. You can switch incrementally. Create a few databases by hand in the dashboard, point test clients at their subdomain URLs, and confirm everything works before shifting production traffic over. Enable **Auto Create** in your project **Settings** before pointing clients at new database subdomains, so Lark provisions each one on first connection. # Flutter SDK Source: https://docs.larksh.com/firebase/flutter Use the Firebase Flutter SDK with Lark Point your Firebase Flutter SDK at Lark by passing your Lark URL to `FirebaseDatabase.instanceFor()`. Everything else (listeners, writes, queries) works without changes. ## Setup Add the Realtime Database plugin to your `pubspec.yaml`: ```yaml theme={null} dependencies: firebase_core: ^3.0.0 firebase_database: ^11.0.0 ``` Then run: ```bash theme={null} flutter pub get ``` ## Connect to Lark Pass your Lark URL when getting the database instance: ```dart theme={null} import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_database/firebase_database.dart'; await Firebase.initializeApp(); // Change this from https://PROJECT_ID.firebaseio.com final database = FirebaseDatabase.instanceFor( app: Firebase.app(), databaseURL: 'https://your-lark-project-id.larkdb.net', ); final ref = database.ref('players/alice'); ``` If your `firebase_options.dart` (generated by FlutterFire CLI) already has the `databaseURL`, you can update it there instead and use `FirebaseDatabase.instance` directly. ## Database routing If your Firebase app stores many independent data silos under path prefixes (game rooms, workspaces, etc.), see [Database routing](/firebase/database-routing) for options on splitting them into separate Lark databases. # iOS SDK Source: https://docs.larksh.com/firebase/ios Use the Firebase Apple SDK with Lark Point your Firebase Apple SDK at Lark by passing your Lark URL to `Database.database(url:)`. Everything else (observers, writes, queries) works without changes. ## Setup Add Firebase Realtime Database via Swift Package Manager or CocoaPods: ```swift Swift Package Manager theme={null} // In Xcode: File > Add Package Dependencies // Enter: https://github.com/firebase/firebase-ios-sdk // Select "FirebaseDatabase" ``` ```ruby CocoaPods theme={null} pod 'Firebase/Database' ``` ## Connect to Lark Pass your Lark URL when getting the database instance: ```swift theme={null} import FirebaseDatabase // Change this from https://PROJECT_ID.firebaseio.com let database = Database.database(url: "https://your-lark-project-id.larkdb.net") let ref = database.reference() ``` If your `GoogleService-Info.plist` already has the `DATABASE_URL` field, you can update it there instead and use `Database.database()` without arguments. ## Database routing If your Firebase app stores many independent data silos under path prefixes (game rooms, workspaces, etc.), see [Database routing](/firebase/database-routing) for options on splitting them into separate Lark databases. # JavaScript SDK Source: https://docs.larksh.com/firebase/javascript Use the Firebase JavaScript SDK with Lark The only change you need is the `databaseURL` in your Firebase config. Here's a typical config object. The highlighted line is the only thing you change: ```javascript theme={null} var firebaseConfig = { apiKey: "API_KEY", authDomain: "PROJECT_ID.firebaseapp.com", // Change this from https://PROJECT_ID.firebaseio.com databaseURL: "https://your-lark-project-id.larkdb.net", projectId: "PROJECT_ID", storageBucket: "PROJECT_ID.firebasestorage.app", messagingSenderId: "SENDER_ID", appId: "APP_ID", measurementId: "G-MEASUREMENT_ID", }; ``` ## Firebase JS SDK v8 ```javascript theme={null} import firebase from 'firebase/app'; import 'firebase/database'; firebase.initializeApp({ databaseURL: 'https://your-lark-project-id.larkdb.net' }); const db = firebase.database(); ``` ## Firebase JS SDK v9 (modular) ```javascript theme={null} import { initializeApp } from 'firebase/app'; import { getDatabase, ref, set } from 'firebase/database'; const app = initializeApp({ databaseURL: 'https://your-lark-project-id.larkdb.net' }); const db = getDatabase(app); ``` ## Database routing If your Firebase app stores many independent data silos under path prefixes (game rooms, workspaces, etc.), see [Database routing](/firebase/database-routing) for options on splitting them into separate Lark databases. # Migrating from Firebase Source: https://docs.larksh.com/firebase/migration Step-by-step guide to moving your Firebase Realtime Database to Lark Moving from Firebase Realtime Database to Lark is straightforward. Your code, security rules, and data model all carry over. Create a project in the [Lark dashboard](https://dashboard.lark.sh). Note your project ID, as you'll need it for the connection URL. If your Firebase app stores multiple independent data silos (game rooms, workspaces, etc.) under path prefixes, Lark can split them into separate databases automatically. See [Database routing](/firebase/database-routing) for your options. If you use Firebase Auth, enter your Firebase project ID in the **Firebase Auth Project ID** field. This lets Lark validate your users' tokens. See [Firebase Auth with Lark](/firebase/auth) for details. Copy your Firebase RTDB security rules into Lark's security rules editor. The syntax is identical. Paste them in and click **Save Rules**. You can find your current rules in the Firebase console under Realtime Database > Rules. In the Firebase console, export your database as JSON. Or use the Firebase CLI: ```bash theme={null} firebase database:get / > backup.json ``` For large databases, consider exporting individual paths to keep file sizes manageable. In the Lark dashboard, open your database and use **Import** to upload the JSON file. This writes the data to your Lark database in the same structure it had in Firebase. Change your `databaseURL` from: ``` https://your-project.firebaseio.com ``` to: ``` https://your-lark-project-id.larkdb.net ``` This is the only code change required. Run your app and verify that reads, writes, and subscriptions work as expected. Start with a staging environment or a small percentage of production traffic. You don't have to migrate all at once. Start by pointing a staging environment at Lark, run your test suite, then gradually shift production traffic. ## Common gotchas A few things to double-check before you go live: * Make sure **Ephemeral mode** is OFF if you want data to persist. Ephemeral mode discards data when all subscribers disconnect. Great for presence systems, not great for your users table. * If you're using [database routing](/firebase/database-routing) to split data into separate databases, enable **Auto-create** so Lark creates databases on first connect. * Test your security rules in Lark's rules editor before going live. The syntax is the same, but verify that everything was copied correctly. * If you have high-frequency data like cursor positions or player movement, consider configuring [volatile paths](/platform/volatile). Firebase SDK clients fully support volatile updates. Lark batches them on the server and delivers them as `PATCH` events that the Firebase SDK handles natively. You just need to define the volatile path patterns in your project settings. ## What's next Full list of what's supported and any differences. Split your single Firebase database into multiple Lark databases. Keep using Firebase Auth tokens with Lark. Add high-frequency updates to your migrated app. # Using Lark with Firebase SDKs Source: https://docs.larksh.com/firebase/overview Use your existing Firebase Realtime Database code with Lark Lark is wire-compatible with Firebase Realtime Database. If you have an existing Firebase RTDB app, you can point it at Lark with minimal changes. ## What this means The official Firebase SDKs can connect to Lark instead of Firebase. Your existing code (reads, writes, subscriptions, queries, transactions) works as-is. You don't need to rewrite anything. Lark implements the same wire protocol that Firebase RTDB uses. When the Firebase SDK opens a WebSocket connection, it doesn't know (or care) that it's talking to Lark instead of Google's servers. The messages are identical. The [REST API](/rest-api/overview) is also fully compatible. If your backend hits Firebase's `/.json` endpoints, change the hostname and it works with Lark. ## What's different Not much. Three things change: 1. You point at `larkdb.net` instead of `firebaseio.com` for the connection URL. 2. If you use Firebase Auth, you tell Lark your Firebase project ID so it can validate tokens. 3. A few edge cases, documented in the [compatibility notes](/firebase/compatibility). ## What's next Set up the Firebase JS SDK with Lark. Set up the Firebase Android SDK with Lark. Set up the Firebase Apple SDK with Lark. Step-by-step migration guide. # Unity SDK Source: https://docs.larksh.com/firebase/unity Use the Firebase Unity SDK with Lark Point your Firebase Unity SDK at Lark by passing your Lark URL to `FirebaseDatabase.GetInstance()`. Everything else (listeners, writes, transactions) works without changes. ## Setup Add the Firebase Realtime Database package to your Unity project: 1. Download the [Firebase Unity SDK](https://firebase.google.com/download/unity) 2. Import `FirebaseDatabase.unitypackage` via **Assets > Import Package > Custom Package** 3. Add your `google-services.json` (Android) or `GoogleService-Info.plist` (iOS) to the project ## Connect to Lark Pass your Lark URL when getting the database instance: ```csharp theme={null} using Firebase.Database; // Change this from https://PROJECT_ID.firebaseio.com FirebaseDatabase database = FirebaseDatabase.GetInstance("https://your-lark-project-id.larkdb.net"); DatabaseReference myRef = database.GetReference("players/alice"); ``` If your config file already has the database URL, you can update it there instead and use `FirebaseDatabase.DefaultInstance` directly. ## Database routing If your Firebase app stores many independent data silos under path prefixes (game rooms, workspaces, etc.), see [Database routing](/firebase/database-routing) for options on splitting them into separate Lark databases. # What is Lark? Source: https://docs.larksh.com/index Interactive everything for the modern era Lark is the realtime database for web apps, games, and mobile. You write data, your users see it instantly, across every connected client, lightning fast. If you've ever used Firebase Realtime Database, Lark will feel familiar. But Lark is built from the ground up in Rust, with the performance and features that today's multiplayer games and collaborative apps demand. ## Features Every write is broadcast to every subscriber in real time. No polling, no manual refresh. Your UI stays in sync automatically. We want Lark to be sustainably operated and grow with our builders. The features and performance you need at a price that makes sense. The Lark JS SDK is \~20KB gzipped. Install it, connect, and start building. No heavyweight framework required. Millisecond latency volatile updates and support for UDP-based transports like WebTransport and KCP for demanding real-time apps & games. Already using Firebase Realtime Database? Point your existing Firebase SDK at Lark and it just works. Change one URL. Declarative, path-based security rules that run on the server. Control who can read and write what, with full access to auth context and existing data. ## How it works Lark stores your data as a JSON tree. You read and write to paths in that tree, and Lark handles synchronization across all connected clients. ```typescript theme={null} import { LarkDatabase } from '@lark-sh/client'; const db = new LarkDatabase('my-project/my-database', { anonymous: true }); // Write data await db.ref('players/alice').set({ name: 'Alice', score: 0 }); // Subscribe to real-time updates db.ref('players').on('value', (snapshot) => { console.log('Players updated:', snapshot.val()); }); // Every connected client sees the change instantly await db.ref('players/alice/score').set(42); ``` ## Get started Create a project and build your first real-time app in minutes. Already on Firebase? Learn how to point your existing code at Lark. Get started with the native Lark JavaScript/TypeScript SDK. Read and write data over HTTP. No SDK or persistent connection required. Explore the Lark dashboard: manage projects, databases, rules, and metrics. # DataSnapshot Source: https://docs.larksh.com/lark-sdk/api/data-snapshot API reference for the DataSnapshot class A `DataSnapshot` is an immutable representation of data at a specific location and point in time. Snapshots are returned by read operations such as [`once()`](/lark-sdk/api/database-reference#once), [`get()`](/lark-sdk/api/database-reference#get), and [`on()`](/lark-sdk/api/database-reference#on) callbacks. ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); if (snapshot.exists()) { console.log(snapshot.val()); } ``` ## Properties | Property | Type | Description | | -------- | ------------------- | ---------------------------------------------------------------------------------------------------- | | `key` | `string \| null` | The key (last segment of the path) of the location this snapshot represents, or `null` for the root. | | `ref` | `DatabaseReference` | The `DatabaseReference` for the location this snapshot was read from. | ## Methods ### val ```typescript theme={null} val(): any ``` Returns the data contained in this snapshot as a JavaScript value (object, array, string, number, boolean, or `null`). Priority metadata (`.priority`) is stripped from the result. Returns `null` if no data exists at this location. ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); const user = snapshot.val(); // { name: 'Alice', email: 'alice@example.com' } ``` ### exists ```typescript theme={null} exists(): boolean ``` Returns `true` if this snapshot contains data (i.e., `val()` would return something other than `null`). ```typescript theme={null} const snapshot = await db.ref('users/bob').get(); if (!snapshot.exists()) { console.log('User not found'); } ``` ### hasChildren ```typescript theme={null} hasChildren(): boolean ``` Returns `true` if this snapshot has any child properties. ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); if (snapshot.hasChildren()) { console.log(`Has ${snapshot.numChildren()} children`); } ``` ### hasChild ```typescript theme={null} hasChild(path: string): boolean ``` Returns `true` if a child exists at the specified relative path. | Parameter | Type | Description | | --------- | -------- | --------------------------- | | `path` | `string` | The relative path to check. | ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); if (snapshot.hasChild('profile/avatar')) { console.log('User has an avatar'); } ``` ### numChildren ```typescript theme={null} numChildren(): number ``` Returns the number of immediate children of this snapshot. ```typescript theme={null} const snapshot = await db.ref('users').get(); console.log(`Total users: ${snapshot.numChildren()}`); ``` ### child ```typescript theme={null} child(path: string): DataSnapshot ``` Returns a `DataSnapshot` for a child location relative to this snapshot. | Parameter | Type | Description | | --------- | -------- | ------------------------------- | | `path` | `string` | The relative path to the child. | ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); const name = snapshot.child('name').val(); // 'Alice' ``` ### forEach ```typescript theme={null} forEach(callback: (child: DataSnapshot) => boolean | void): void ``` Iterates over each direct child of this snapshot in sorted order. If the callback returns `true`, iteration stops early. | Parameter | Type | Description | | ---------- | ------------------------------------------ | ------------------------------------------------------------ | | `callback` | `(child: DataSnapshot) => boolean \| void` | Called once for each child. Return `true` to stop iterating. | ```typescript theme={null} const snapshot = await db.ref('users').get(); snapshot.forEach((childSnapshot) => { console.log(`${childSnapshot.key}: ${childSnapshot.val().name}`); }); ``` Children are iterated in their sort order. If the snapshot was produced by a query with an `orderBy` clause, `forEach` respects that ordering. ### getPriority ```typescript theme={null} getPriority(): string | number | null ``` Returns the priority of the data at this location, or `null` if no priority is set. ```typescript theme={null} const snapshot = await db.ref('scores/alice').get(); const priority = snapshot.getPriority(); // e.g. 100 ``` ### exportVal ```typescript theme={null} exportVal(): any ``` Returns the data including priority metadata (`.priority` and `.value` fields). Useful for serialization or backup scenarios where priority information must be preserved. ```typescript theme={null} const snapshot = await db.ref('scores/alice').get(); const exported = snapshot.exportVal(); // { '.value': { score: 100 }, '.priority': 100 } ``` ### toJSON ```typescript theme={null} toJSON(): any ``` Returns the same result as `exportVal()`. Provided for compatibility with `JSON.stringify()`. ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); const json = JSON.stringify(snapshot); // uses toJSON() internally ``` ### isVolatile ```typescript theme={null} isVolatile(): boolean ``` Returns `true` if this snapshot was produced by a high-frequency volatile update. Volatile snapshots are delivered outside the normal consistency model for low-latency use cases such as cursor positions or live indicators. ```typescript theme={null} db.ref('cursors').on('value', (snapshot) => { if (snapshot.isVolatile()) { // High-frequency update — render immediately renderCursors(snapshot.val()); } }); ``` ### getServerTimestamp ```typescript theme={null} getServerTimestamp(): number | null ``` Returns the server timestamp (in milliseconds since the Unix epoch) associated with this snapshot, or `null` if not available. Primarily useful for volatile snapshots to determine when the update was generated on the server. ```typescript theme={null} db.ref('cursors').on('value', (snapshot) => { const ts = snapshot.getServerTimestamp(); if (ts) { console.log(`Server time: ${new Date(ts).toISOString()}`); } }); ``` # DatabaseReference Source: https://docs.larksh.com/lark-sdk/api/database-reference API reference for the DatabaseReference class A `DatabaseReference` represents a specific path in the database. It provides methods for reading, writing, and querying data at that location. Obtain a reference via [`LarkDatabase.ref()`](/lark-sdk/api/lark-database#ref). ```typescript theme={null} const ref = db.ref('users/alice'); ``` ## Properties | Property | Type | Description | | ----------------- | --------------------------- | ---------------------------------------------------------------------------------- | | `path` | `string` | The absolute path this reference points to. | | `key` | `string \| null` | The last segment of the path, or `null` for the root reference. | | `parent` | `DatabaseReference \| null` | A reference to the parent location, or `null` for the root. | | `root` | `DatabaseReference` | A reference to the root of the database. | | `database` | `LarkDatabase` | The `LarkDatabase` instance this reference belongs to. | | `queryIdentifier` | `string` | A string that uniquely identifies the query parameters attached to this reference. | ## Navigation ### child ```typescript theme={null} child(path: string): DatabaseReference ``` Returns a new `DatabaseReference` for a child location relative to this reference. | Parameter | Type | Description | | --------- | -------- | ----------------------------------------------------------------------------------- | | `path` | `string` | A relative path from this location. Can contain multiple segments separated by `/`. | ```typescript theme={null} const usersRef = db.ref('users'); const aliceRef = usersRef.child('alice'); const profileRef = usersRef.child('alice/profile'); ``` ## Write Methods ### set ```typescript theme={null} set(value: any): Promise ``` Writes a value to this location, replacing any existing data. | Parameter | Type | Description | | --------- | ----- | ------------------------------------------------------------------------------------------ | | `value` | `any` | The value to write. Can be a primitive, object, array, or `null` (which deletes the data). | ```typescript theme={null} await db.ref('users/alice').set({ name: 'Alice', email: 'alice@example.com', }); ``` ### update ```typescript theme={null} update(values: object): Promise ``` Updates specific children at this location without overwriting the entire node. Supports multi-path updates with nested path keys. | Parameter | Type | Description | | --------- | -------- | -------------------------------------------- | | `values` | `object` | An object containing the children to update. | ```typescript theme={null} await db.ref('users/alice').update({ email: 'newalice@example.com', 'profile/lastSeen': Date.now(), }); ``` ### remove ```typescript theme={null} remove(): Promise ``` Deletes the data at this location and all child locations. ```typescript theme={null} await db.ref('users/alice').remove(); ``` ### push ```typescript theme={null} push(value?: any): Promise ``` Generates a new child location with a unique, chronologically-sortable key and optionally writes a value to it. Returns a reference to the new child. | Parameter | Type | Description | | --------- | ----- | -------------------------------------------------- | | `value` | `any` | Optional value to write at the new child location. | ```typescript theme={null} const newPostRef = await db.ref('posts').push({ title: 'Hello World', timestamp: ServerValue.TIMESTAMP, }); console.log(newPostRef.key); // e.g. "-NxK3j..." ``` If you omit the value, `push()` returns a reference with the generated key without writing anything. You can then use `set()` on the returned reference. ### setWithPriority ```typescript theme={null} setWithPriority(value: any, priority: string | number | null): Promise ``` Writes a value along with a priority for sorting. | Parameter | Type | Description | | ---------- | -------------------------- | ------------------- | | `value` | `any` | The value to write. | | `priority` | `string \| number \| null` | The sort priority. | ```typescript theme={null} await db.ref('scores/alice').setWithPriority({ score: 100 }, 100); ``` ### setPriority ```typescript theme={null} setPriority(priority: string | number | null): Promise ``` Sets the priority of data at this location without modifying the value. | Parameter | Type | Description | | ---------- | -------------------------- | ----------------- | | `priority` | `string \| number \| null` | The new priority. | ```typescript theme={null} await db.ref('scores/alice').setPriority(200); ``` ## Read Methods ### once ```typescript theme={null} once(eventType?: string): Promise ``` Reads data from this location once. Does not subscribe to ongoing changes. | Parameter | Type | Default | Description | | ----------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------- | | `eventType` | `string` | `'value'` | The event type to listen for. One of `'value'`, `'child_added'`, `'child_changed'`, `'child_removed'`, `'child_moved'`. | ```typescript theme={null} const snapshot = await db.ref('users/alice').once(); console.log(snapshot.val()); ``` ### get ```typescript theme={null} get(): Promise ``` Reads the data at this location once. Equivalent to `once('value')`. ```typescript theme={null} const snapshot = await db.ref('users/alice').get(); if (snapshot.exists()) { console.log(snapshot.val()); } ``` ## Subscription Methods ### on ```typescript theme={null} on(eventType: string, callback: (snapshot: DataSnapshot) => void): () => void ``` Subscribes to data changes at this location. The callback fires immediately with the current value and again whenever the data changes. Returns an unsubscribe function. | Parameter | Type | Description | | ----------- | ---------- | ------------------------------------------------------- | | `eventType` | `string` | The event type to listen for. | | `callback` | `Function` | Called with a `DataSnapshot` each time the event fires. | **Event types:** | Event | Description | | ----------------- | ------------------------------------------------------------------ | | `'value'` | Fires when the data at this location (including children) changes. | | `'child_added'` | Fires for each existing child and when a new child is added. | | `'child_changed'` | Fires when a child's data changes. | | `'child_removed'` | Fires when a child is removed. | | `'child_moved'` | Fires when a child's sort order changes. | ```typescript theme={null} const unsubscribe = db.ref('users').on('child_added', (snapshot) => { console.log(`User added: ${snapshot.key}`, snapshot.val()); }); // Stop listening unsubscribe(); ``` ## Query Methods All query methods return a new `DatabaseReference` with query constraints attached. They do not modify the original reference. ### orderByChild ```typescript theme={null} orderByChild(path: string): DatabaseReference ``` Orders results by the value of a specified child key. | Parameter | Type | Description | | --------- | -------- | ----------------------------------------------------------------------- | | `path` | `string` | The child key to order by. Can be a nested path (e.g. `'profile/age'`). | ```typescript theme={null} const byAge = db.ref('users').orderByChild('age').limitToFirst(10); ``` ### orderByKey ```typescript theme={null} orderByKey(): DatabaseReference ``` Orders results by their key. ```typescript theme={null} const byKey = db.ref('users').orderByKey(); ``` ### orderByValue ```typescript theme={null} orderByValue(): DatabaseReference ``` Orders results by their value. Useful when children are primitives. ```typescript theme={null} const byScore = db.ref('scores').orderByValue().limitToLast(5); ``` ### orderByPriority ```typescript theme={null} orderByPriority(): DatabaseReference ``` Orders results by their priority. ```typescript theme={null} const byPriority = db.ref('tasks').orderByPriority(); ``` ### limitToFirst ```typescript theme={null} limitToFirst(limit: number): DatabaseReference ``` Limits the result set to the first N items (based on the current ordering). | Parameter | Type | Description | | --------- | -------- | ----------------------------------------------------- | | `limit` | `number` | Maximum number of items to return from the beginning. | ```typescript theme={null} const firstFive = db.ref('posts').orderByKey().limitToFirst(5); ``` ### limitToLast ```typescript theme={null} limitToLast(limit: number): DatabaseReference ``` Limits the result set to the last N items (based on the current ordering). | Parameter | Type | Description | | --------- | -------- | ----------------------------------------------- | | `limit` | `number` | Maximum number of items to return from the end. | ```typescript theme={null} const lastTen = db.ref('posts').orderByKey().limitToLast(10); ``` ### startAt ```typescript theme={null} startAt(value: any, key?: string): DatabaseReference ``` Returns items starting at the specified value (inclusive). | Parameter | Type | Description | | --------- | -------- | -------------------------------------------------------------------------- | | `value` | `any` | The value to start at. | | `key` | `string` | Optional key to further filter when multiple children have the same value. | ```typescript theme={null} const adults = db.ref('users').orderByChild('age').startAt(18); ``` ### startAfter ```typescript theme={null} startAfter(value: any, key?: string): DatabaseReference ``` Returns items starting strictly after the specified value (exclusive). | Parameter | Type | Description | | --------- | -------- | ----------------------------------- | | `value` | `any` | The value to start after. | | `key` | `string` | Optional key for further filtering. | ```typescript theme={null} const olderThan18 = db.ref('users').orderByChild('age').startAfter(18); ``` ### endAt ```typescript theme={null} endAt(value: any, key?: string): DatabaseReference ``` Returns items ending at the specified value (inclusive). | Parameter | Type | Description | | --------- | -------- | ----------------------------------- | | `value` | `any` | The value to end at. | | `key` | `string` | Optional key for further filtering. | ```typescript theme={null} const upTo30 = db.ref('users').orderByChild('age').endAt(30); ``` ### endBefore ```typescript theme={null} endBefore(value: any, key?: string): DatabaseReference ``` Returns items ending strictly before the specified value (exclusive). | Parameter | Type | Description | | --------- | -------- | ----------------------------------- | | `value` | `any` | The value to end before. | | `key` | `string` | Optional key for further filtering. | ```typescript theme={null} const under30 = db.ref('users').orderByChild('age').endBefore(30); ``` ### equalTo ```typescript theme={null} equalTo(value: any, key?: string): DatabaseReference ``` Returns only items matching the specified value exactly. | Parameter | Type | Description | | --------- | -------- | ----------------------------------- | | `value` | `any` | The value to match. | | `key` | `string` | Optional key for further filtering. | ```typescript theme={null} const admins = db.ref('users').orderByChild('role').equalTo('admin'); ``` ## Other Methods ### onDisconnect ```typescript theme={null} onDisconnect(): OnDisconnect ``` Returns an [`OnDisconnect`](/lark-sdk/api/ondisconnect) object that lets you register write operations to be performed on the server if the client disconnects. ```typescript theme={null} const presenceRef = db.ref('status/alice'); presenceRef.onDisconnect().set('offline'); await presenceRef.set('online'); ``` ### transaction ```typescript theme={null} transaction(updateFn: (currentValue: any) => any): Promise ``` Atomically reads and modifies data at this location. The `updateFn` receives the current value and returns the desired new value. If another client writes concurrently, the function is retried with the updated value. | Parameter | Type | Description | | ---------- | ---------------------------- | ----------------------------------------------------------------------------------------------- | | `updateFn` | `(currentValue: any) => any` | A function that takes the current value and returns the new value. Return `undefined` to abort. | ```typescript theme={null} const result = await db.ref('counters/likes').transaction((current) => { return (current || 0) + 1; }); console.log(result.committed); // true if the transaction succeeded console.log(result.snapshot.val()); // the final value ``` # LarkDatabase Source: https://docs.larksh.com/lark-sdk/api/lark-database API reference for the LarkDatabase class The `LarkDatabase` class is the main entry point for interacting with a Lark database. It manages the connection lifecycle, authentication, and provides access to database references. ## Constructor ```typescript theme={null} new LarkDatabase(projectAndDatabase: string, options?: LarkDatabaseOptions) ``` Creates a new `LarkDatabase` instance. | Parameter | Type | Description | | -------------------- | --------------------- | ------------------------------------------------------------- | | `projectAndDatabase` | `string` | A combined identifier in the format `'projectId/databaseId'`. | | `options` | `LarkDatabaseOptions` | Optional configuration object. | ### LarkDatabaseOptions | Option | Type | Default | Description | | --------------------- | ----------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------ | | `anonymous` | `boolean` | `false` | Connect without user credentials. | | `token` | `string` | — | A JWT for authenticated access. | | `domain` | `string` | `'larkdb.net'` | The Lark server domain. | | `transport` | `'auto' \| 'websocket' \| 'webtransport'` | `'websocket'` | Which transport protocol to use. | | `webtransportTimeout` | `number` | `2000` | Milliseconds to wait for WebTransport before falling back to WebSocket. Only applies when `transport` is `'auto'`. | You must provide either `anonymous: true` or a `token`. If you pass both, the token takes priority. ```typescript theme={null} import { LarkDatabase } from '@lark-sh/client'; const db = new LarkDatabase('my-project/my-database', { anonymous: true }); ``` ## Properties | Property | Type | Description | | ------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `connected` | `boolean` | Whether the client is currently connected to the server. | | `state` | `ConnectionState` | The current connection state. One of `'disconnected'`, `'connecting'`, `'connected'`, `'joined'`, `'authenticated'`, or `'reconnecting'`. | | `auth` | `AuthInfo \| null` | The current authentication info, or `null` if not signed in. | | `reconnecting` | `boolean` | Whether the client is currently attempting to reconnect. | | `transportType` | `'websocket' \| 'webtransport' \| null` | The active transport protocol, or `null` if not connected. | | `serverTimeOffset` | `number` | Estimated offset in milliseconds between the client clock and the server clock. | | `volatilePaths` | `string[]` | List of paths configured for high-frequency volatile updates. | ### AuthInfo | Property | Type | Description | | ---------- | -------- | ------------------------------------------- | | `uid` | `string` | The authenticated user's unique identifier. | | `provider` | `string` | The authentication provider used. | | `token` | `string` | The current authentication token. | ## Methods ### connect ```typescript theme={null} connect(): Promise ``` Opens a connection to the Lark server using the options passed to the constructor. Resolves when the connection is fully established and authenticated. ```typescript theme={null} const db = new LarkDatabase('my-project/my-database', { anonymous: true }); await db.connect(); // optional, operations queue automatically ``` ### disconnect ```typescript theme={null} disconnect(): Promise ``` Closes the connection to the server. Resolves when the connection is fully closed. ```typescript theme={null} await db.disconnect(); ``` ### ref ```typescript theme={null} ref(path?: string): DatabaseReference ``` Returns a `DatabaseReference` pointing to the specified path in the database. If no path is provided, returns a reference to the root. | Parameter | Type | Description | | --------- | -------- | -------------------------------------------- | | `path` | `string` | Optional path relative to the database root. | ```typescript theme={null} const usersRef = db.ref('users'); const rootRef = db.ref(); const nestedRef = db.ref('users/alice/profile'); ``` ### signIn ```typescript theme={null} signIn(token: string): Promise ``` Authenticates the client with the given token. Resolves when authentication succeeds. | Parameter | Type | Description | | --------- | -------- | ------------------------ | | `token` | `string` | An authentication token. | ```typescript theme={null} await db.signIn('eyJhbGciOi...'); ``` ### signOut ```typescript theme={null} signOut(): Promise ``` Signs out the current user and reverts to unauthenticated access. ```typescript theme={null} await db.signOut(); ``` ### transaction ```typescript theme={null} transaction(ops: TransactionOperation[]): Promise ``` Executes a batch of operations atomically. All operations succeed or all fail together. | Parameter | Type | Description | | --------- | ------------------------ | --------------------------------------------- | | `ops` | `TransactionOperation[]` | An array of operations to execute atomically. | ```typescript theme={null} const result = await db.transaction([ { type: 'set', path: 'counters/a', value: 1 }, { type: 'set', path: 'counters/b', value: 2 }, ]); ``` ### goOffline ```typescript theme={null} goOffline(): void ``` Manually disconnects the client and disables automatic reconnection. Pending writes are preserved. ```typescript theme={null} db.goOffline(); ``` ### goOnline ```typescript theme={null} goOnline(): void ``` Re-enables the connection after a `goOffline()` call. The client will attempt to reconnect and flush any pending writes. ```typescript theme={null} db.goOnline(); ``` ### hasPendingWrites ```typescript theme={null} hasPendingWrites(): boolean ``` Returns `true` if there are writes that have not yet been acknowledged by the server. ```typescript theme={null} if (db.hasPendingWrites()) { console.log('Waiting for writes to sync...'); } ``` ### getPendingWriteCount ```typescript theme={null} getPendingWriteCount(): number ``` Returns the number of write operations that have not yet been acknowledged by the server. ```typescript theme={null} console.log(`${db.getPendingWriteCount()} writes pending`); ``` ### clearPendingWrites ```typescript theme={null} clearPendingWrites(): void ``` Discards all pending writes that have not been sent to the server. Use with caution, because discarded writes cannot be recovered. This permanently drops any unsent writes. Only use this if you intentionally want to discard local changes. ```typescript theme={null} db.clearPendingWrites(); ``` ## Event Methods All event methods return an unsubscribe function. Call it to stop listening. ### onConnect ```typescript theme={null} onConnect(callback: () => void): () => void ``` Registers a listener that fires when the client establishes a connection. ```typescript theme={null} const unsubscribe = db.onConnect(() => { console.log('Connected to Lark'); }); // Later, stop listening unsubscribe(); ``` ### onDisconnect ```typescript theme={null} onDisconnect(callback: () => void): () => void ``` Registers a listener that fires when the client loses its connection. ```typescript theme={null} const unsubscribe = db.onDisconnect(() => { console.log('Disconnected from Lark'); }); ``` ### onReconnecting ```typescript theme={null} onReconnecting(callback: () => void): () => void ``` Registers a listener that fires when the client begins a reconnection attempt. ```typescript theme={null} const unsubscribe = db.onReconnecting(() => { console.log('Attempting to reconnect...'); }); ``` ### onError ```typescript theme={null} onError(callback: (error: LarkError) => void): () => void ``` Registers a listener that fires when a connection-level error occurs. ```typescript theme={null} const unsubscribe = db.onError((error) => { console.error(`Lark error [${error.code}]: ${error.message}`); }); ``` ### onAuthStateChanged ```typescript theme={null} onAuthStateChanged(callback: (auth: AuthInfo | null) => void): () => void ``` Registers a listener that fires when the authentication state changes: on sign-in, sign-out, or token refresh. ```typescript theme={null} const unsubscribe = db.onAuthStateChanged((auth) => { if (auth) { console.log(`Signed in as ${auth.uid}`); } else { console.log('Signed out'); } }); ``` # LarkError Source: https://docs.larksh.com/lark-sdk/api/lark-error API reference for the LarkError class `LarkError` extends the native `Error` class with a `code` property for programmatic error handling. All errors thrown or surfaced by the Lark SDK are instances of `LarkError`. ```typescript theme={null} import { LarkError } from '@lark-sh/client'; try { await db.ref('secret/data').get(); } catch (error) { if (error instanceof LarkError) { console.error(`[${error.code}] ${error.message}`); } } ``` ## Properties | Property | Type | Description | | --------- | -------- | ------------------------------------------------------------ | | `code` | `string` | A machine-readable error code identifying the type of error. | | `message` | `string` | A human-readable description of the error. | ## Error Codes | Code | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------- | | `permission_denied` | The client does not have permission to perform the requested operation. Check your security rules. | | `invalid_data` | The data provided is invalid or cannot be stored (e.g., contains unsupported types or exceeds size limits). | | `not_found` | The requested resource does not exist. | | `invalid_path` | The specified path is malformed or contains illegal characters. | | `timeout` | The operation timed out before completing. This can occur during reads, writes, or connection attempts. | | `not_connected` | The operation requires an active connection, but the client is not connected to the server. | | `condition_failed` | A transaction or conditional write failed because the underlying data was modified by another client. | | `max_retries_exceeded` | A transaction exceeded the maximum number of retry attempts without successfully committing. | | `write_tainted` | A pending write was invalidated because it conflicts with a server-side change. | | `view_recovering` | The local view of the data is temporarily inconsistent and is being recovered. Retry the operation shortly. | | `auth_required` | The operation requires authentication, but the client is not signed in. Call `signIn()` first. | ## Handling Errors ### With try/catch ```typescript theme={null} try { await db.ref('protected/path').set({ key: 'value' }); } catch (error) { if (error instanceof LarkError) { switch (error.code) { case 'permission_denied': console.error('Access denied. Please sign in.'); break; case 'not_connected': console.error('No connection. Retrying when online.'); break; default: console.error(`Unexpected error: ${error.code}`); } } } ``` ### With the error event Connection-level errors are emitted via the `onError` event on the `LarkDatabase` instance. ```typescript theme={null} db.onError((error: LarkError) => { console.error(`Connection error [${error.code}]: ${error.message}`); }); ``` Transaction-related codes (`condition_failed`, `max_retries_exceeded`, `write_tainted`) indicate that the transaction could not commit. In most cases the SDK retries automatically, but if `max_retries_exceeded` is thrown, you may need to retry with different logic. # OnDisconnect Source: https://docs.larksh.com/lark-sdk/api/ondisconnect API reference for the OnDisconnect class The `OnDisconnect` class lets you register write operations that the server will execute if the client disconnects unexpectedly. This is commonly used for presence systems, cleanup tasks, and ensuring data consistency when a user goes offline. Obtain an `OnDisconnect` instance via [`DatabaseReference.onDisconnect()`](/lark-sdk/api/database-reference#ondisconnect). ```typescript theme={null} const presenceRef = db.ref('status/alice'); presenceRef.onDisconnect().set('offline'); await presenceRef.set('online'); ``` All `OnDisconnect` methods return a `Promise` that resolves once the server has acknowledged and registered the operation. The operation itself executes later, only if the client disconnects. ## Methods ### set ```typescript theme={null} set(value: any): Promise ``` Registers a `set` operation to run on disconnect. The specified value will replace any data at this location when the client disconnects. | Parameter | Type | Description | | --------- | ----- | ----------------------------------------------------------------- | | `value` | `any` | The value to write on disconnect. Pass `null` to delete the data. | ```typescript theme={null} const ref = db.ref('rooms/room1/members/alice'); await ref.onDisconnect().set(null); await ref.set({ name: 'Alice', joinedAt: ServerValue.TIMESTAMP }); ``` ### update ```typescript theme={null} update(values: object): Promise ``` Registers an `update` operation to run on disconnect. Only the specified children are modified; other children at this location are left intact. | Parameter | Type | Description | | --------- | -------- | ---------------------------------------------------------- | | `values` | `object` | An object containing the children to update on disconnect. | ```typescript theme={null} await db.ref('users/alice').onDisconnect().update({ status: 'offline', lastSeen: ServerValue.TIMESTAMP, }); ``` ### remove ```typescript theme={null} remove(): Promise ``` Registers a `remove` operation to run on disconnect. All data at this location (and its children) will be deleted when the client disconnects. ```typescript theme={null} const memberRef = db.ref('rooms/room1/members/alice'); await memberRef.onDisconnect().remove(); await memberRef.set({ name: 'Alice' }); ``` ### setWithPriority ```typescript theme={null} setWithPriority(value: any, priority: string | number | null): Promise ``` Registers a `set` operation with a priority to run on disconnect. | Parameter | Type | Description | | ---------- | -------------------------- | --------------------------------- | | `value` | `any` | The value to write on disconnect. | | `priority` | `string \| number \| null` | The priority to assign. | ```typescript theme={null} await db.ref('status/alice').onDisconnect().setWithPriority('offline', 0); ``` ### cancel ```typescript theme={null} cancel(): Promise ``` Cancels all previously registered `OnDisconnect` operations at this location. The server will no longer perform any disconnect writes for this reference. ```typescript theme={null} const ref = db.ref('status/alice'); // Register a disconnect handler await ref.onDisconnect().set('offline'); // Later, cancel it await ref.onDisconnect().cancel(); ``` ## Usage Pattern A typical presence system combines `onDisconnect` with a normal write to track whether a user is online. ```typescript theme={null} async function setupPresence(db: LarkDatabase, userId: string): Promise { const statusRef = db.ref(`status/${userId}`); const connectedRef = db.ref('.info/connected'); connectedRef.on('value', async (snapshot) => { if (!snapshot.val()) { return; } await statusRef.onDisconnect().set({ state: 'offline', lastSeen: ServerValue.TIMESTAMP, }); await statusRef.set({ state: 'online', lastSeen: ServerValue.TIMESTAMP, }); }); } ``` # Utilities Source: https://docs.larksh.com/lark-sdk/api/utilities Helper functions and constants exported by the Lark SDK The `@lark-sh/client` package exports several utility functions and constants for working with paths, IDs, and server values. ```typescript theme={null} import { generatePushId, ServerValue, normalizePath, joinPath, getParentPath, getKey, isVolatilePath, } from '@lark-sh/client'; ``` ## generatePushId ```typescript theme={null} generatePushId(): string ``` Generates a unique, chronologically-sortable 20-character ID. The format is identical to the keys produced by [`DatabaseReference.push()`](/lark-sdk/api/database-reference#push). IDs are ordered by time, so items created earlier sort before items created later when using `orderByKey()`. ```typescript theme={null} const id = generatePushId(); // e.g. "-NxK3jR2fHa0YkLm9pQr" await db.ref(`posts/${id}`).set({ title: 'My Post', createdAt: ServerValue.TIMESTAMP, }); ``` Use `generatePushId()` when you need a key before writing, such as when referencing the key in multiple locations within a single `update()` call. ## ServerValue.TIMESTAMP ```typescript theme={null} ServerValue.TIMESTAMP: object ``` A placeholder value that the server replaces with its current timestamp (milliseconds since the Unix epoch) at the time the write is committed. Use this instead of `Date.now()` to ensure consistency across clients with different clock offsets. ```typescript theme={null} await db.ref('users/alice').update({ lastLogin: ServerValue.TIMESTAMP, }); ``` `ServerValue.TIMESTAMP` is a sentinel object, not an actual number. It is resolved server-side when the write is processed. ## normalizePath ```typescript theme={null} normalizePath(path: string): string ``` Normalizes a database path by removing double slashes, trailing slashes, and leading slashes. | Parameter | Type | Description | | --------- | -------- | ---------------------- | | `path` | `string` | The path to normalize. | ```typescript theme={null} normalizePath('//users//alice/'); // "users/alice" normalizePath('/'); // "" ``` ## joinPath ```typescript theme={null} joinPath(...segments: string[]): string ``` Joins one or more path segments with `/`, producing a normalized path. | Parameter | Type | Description | | ------------- | ---------- | ---------------------- | | `...segments` | `string[]` | Path segments to join. | ```typescript theme={null} joinPath('users', 'alice', 'profile'); // "users/alice/profile" joinPath('rooms', 'room1', 'members'); // "rooms/room1/members" ``` ## getParentPath ```typescript theme={null} getParentPath(path: string): string ``` Returns the parent path of the given path. Returns an empty string for top-level paths. | Parameter | Type | Description | | --------- | -------- | ------------------------------ | | `path` | `string` | The path to get the parent of. | ```typescript theme={null} getParentPath('users/alice/profile'); // "users/alice" getParentPath('users'); // "" ``` ## getKey ```typescript theme={null} getKey(path: string): string ``` Returns the last segment of a path (the key). | Parameter | Type | Description | | --------- | -------- | --------------------------------- | | `path` | `string` | The path to extract the key from. | ```typescript theme={null} getKey('users/alice/profile'); // "profile" getKey('users'); // "users" ``` ## isVolatilePath ```typescript theme={null} isVolatilePath(path: string, patterns: string[]): boolean ``` Checks whether a given path matches any of the provided volatile path patterns. Volatile paths are used for high-frequency, low-latency data streams such as cursor positions or live indicators. | Parameter | Type | Description | | ---------- | ---------- | ------------------------------------------------------------------------------------------------------- | | `path` | `string` | The path to check. | | `patterns` | `string[]` | An array of volatile path patterns to match against. Patterns support `*` as a single-segment wildcard. | ```typescript theme={null} const patterns = ['cursors/*', 'typing/*']; isVolatilePath('cursors/alice', patterns); // true isVolatilePath('users/alice', patterns); // false ``` # Authentication Source: https://docs.larksh.com/lark-sdk/auth Authenticate users with the Lark SDK # Authentication Lark supports anonymous and token-based authentication. You choose how to authenticate when you connect, and you can change authentication state after connecting. ## Anonymous authentication The simplest option. Connect without any user identity: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); ``` Anonymous connections are not assigned a UID and auth will be `null` for any security rules checks. You can still choose to let anonymous users read or modify data in your app by setting your security rules appropriately. ## Token-based authentication For identified users, pass a JWT when creating the instance: ```typescript theme={null} const db = new LarkDatabase("my-project/my-database", { token: "eyJhbGciOiJIUzI1NiIs...", }); ``` The token contains the user's identity (UID, provider, custom claims) and is validated by the Lark server. Your security rules can then reference `auth.uid`, `auth.provider`, and any custom claims. See the [platform authentication docs](/platform/auth) for details on how to generate JWTs for your users. ## Changing auth state after connecting You don't have to authenticate at connect time. You can sign in or out at any point after connecting. ### `db.signIn(token)` Authenticate with a new token. If you were previously anonymous or signed in as a different user, the auth state updates: ```typescript theme={null} // Start anonymous const db = new LarkDatabase("my-project/my-database", { anonymous: true }); await db.connect(); // Later, sign in with a token await db.signIn("eyJhbGciOiJIUzI1NiIs..."); ``` ### `db.signOut()` Reverts to anonymous authentication: ```typescript theme={null} await db.signOut(); ``` After calling `signOut()`, the connection stays open. You're still connected, just without user identity. Any subscriptions that depend on authenticated access (via security rules) may stop receiving updates. ## The `auth` property Access the current authentication state at any time: ```typescript theme={null} // When signed in console.log(db.auth); // { uid: "user-123", provider: "custom", token: "eyJ..." } // When anonymous console.log(db.auth); // null ``` | Property | Type | Description | | ---------- | -------- | ----------------------------------------------------------- | | `uid` | `string` | The user's unique identifier. | | `provider` | `string` | The authentication provider (e.g., `"custom"`, `"google"`). | | `token` | `string` | The raw JWT. | ## Listening for auth changes Use `db.onAuthStateChanged(callback)` to react to sign-in and sign-out events: ```typescript theme={null} const unsubscribe = db.onAuthStateChanged((auth) => { if (auth) { console.log("Signed in as:", auth.uid); } else { console.log("Signed out (anonymous)"); } }); // Stop listening unsubscribe(); ``` The callback fires immediately with the current auth state, then again whenever it changes. ## Full example ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // Listen for auth state changes db.onAuthStateChanged((auth) => { if (auth) { console.log(`Welcome, ${auth.uid}`); loadUserData(auth.uid); } else { console.log("Not authenticated"); showLoginScreen(); } }); // When the user logs in through your app's UI async function handleLogin(token: string) { await db.signIn(token); // onAuthStateChanged fires with the new auth state } // When the user logs out async function handleLogout() { await db.signOut(); // onAuthStateChanged fires with null } ``` # Connecting Source: https://docs.larksh.com/lark-sdk/connecting Manage connections, lifecycle, and transport options # Connecting The `LarkDatabase` class is your entry point. Pass your project, database, and auth details to the constructor and start using it immediately. The connection happens automatically. ## Creating an instance The constructor takes a path string in the format `projectId/databaseId`, and an options object: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // Start using it right away — operations queue and run once connected db.ref("users").on("value", (snapshot) => { console.log(snapshot.val()); }); ``` The connection is established lazily. You don't need to call anything else before reading, writing, or subscribing. Operations queue automatically and execute once the connection is authenticated. ## Options All connection configuration is passed as the second argument to the constructor: | Option | Type | Default | Description | | --------------------- | ----------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------ | | `anonymous` | `boolean` | `false` | Connect without user credentials. | | `token` | `string` | — | A JWT for authenticated access. | | `domain` | `string` | `'larkdb.net'` | The Lark server domain. | | `transport` | `'auto' \| 'websocket' \| 'webtransport'` | `'websocket'` | Which transport protocol to use. | | `webtransportTimeout` | `number` | `2000` | Milliseconds to wait for WebTransport before falling back to WebSocket. Only applies when `transport` is `'auto'`. | You must provide either `anonymous: true` or a `token`. If you pass both, the token takes priority. ## Waiting for the connection If you need to confirm the connection is established before proceeding, you can optionally call `await db.connect()`: ```typescript theme={null} const db = new LarkDatabase("my-project/my-database", { anonymous: true }); await db.connect(); // resolves when fully connected and authenticated ``` Most code doesn't need this. Lazy queuing handles it for you. Use `await db.connect()` when you need to gate logic on a confirmed connection (e.g., showing a "connected" indicator, or failing fast if the server is unreachable). ### Transport selection The SDK uses WebSocket by default. To opt in to WebTransport, set `transport` to `'auto'` or `'webtransport'`: ```typescript theme={null} // Default — WebSocket only (you can omit the transport option entirely) const db = new LarkDatabase("my-project/my-database", { anonymous: true, }); // Try WebTransport first, fall back to WebSocket const db = new LarkDatabase("my-project/my-database", { anonymous: true, transport: "auto", }); // Force WebTransport only (will fail if not supported) const db = new LarkDatabase("my-project/my-database", { anonymous: true, transport: "webtransport", }); // Auto with a longer timeout for WebTransport negotiation const db = new LarkDatabase("my-project/my-database", { anonymous: true, transport: "auto", webtransportTimeout: 5000, }); ``` ## Connection states A connection moves through these states in order: ``` disconnected → connecting → connected → joined → authenticated ``` | State | Meaning | | --------------- | ----------------------------------------------------- | | `disconnected` | No connection. Initial state or after `disconnect()`. | | `connecting` | Transport negotiation in progress. | | `connected` | Transport established, handshake in progress. | | `joined` | Joined the database, awaiting authentication. | | `authenticated` | Fully ready. You can read and write data. | ## Properties Once connected, several properties are available on the `db` instance: ```typescript theme={null} // Whether the connection is fully authenticated and ready db.connected; // boolean // Current connection state string db.state; // 'disconnected' | 'connecting' | 'connected' | 'joined' | 'authenticated' // Auth info (null if anonymous) db.auth; // { uid, provider, token } | null // Which transport is active db.transportType; // 'websocket' | 'webtransport' | null // Clock skew between client and server (milliseconds) db.serverTimeOffset; // number // Currently registered volatile paths db.volatilePaths; // string[] ``` ## Lifecycle events Use callback methods to react to connection state changes: ```typescript theme={null} // Fires when the connection is fully established db.onConnect(() => { console.log("Connected and ready"); }); // Fires when the connection drops db.onDisconnect(() => { console.log("Connection lost"); }); // Fires when the SDK is attempting to reconnect db.onReconnecting((attempt) => { console.log(`Reconnection attempt #${attempt}`); }); // Fires on connection-level errors db.onError((error) => { console.error("Connection error:", error.code, error.message); }); ``` All lifecycle callbacks return an unsubscribe function, so you can stop listening when you no longer need them. ```typescript theme={null} const unsubscribe = db.onConnect(() => { console.log("Connected!"); }); // Later, stop listening unsubscribe(); ``` ## Disconnecting There are two ways to take a connection offline, and they behave differently: ### `disconnect()` Fully tears down the connection. Clears all cached data, removes all subscriptions, and resets the client. Use this when you're done with the database entirely. ```typescript theme={null} db.disconnect(); ``` ### `goOffline()` Pauses the connection but preserves your local cache and subscription registrations. When you call `goOnline()` later, subscriptions are re-established and you pick up where you left off. ```typescript theme={null} // Pause db.goOffline(); // Resume later db.goOnline(); ``` After calling `disconnect()`, you need to create a new `LarkDatabase` instance and call `connect()` again to re-establish the connection. After `goOffline()`, a simple `goOnline()` is enough. # Error handling Source: https://docs.larksh.com/lark-sdk/errors Handle errors from the Lark SDK # Error handling The Lark SDK uses a consistent error model. Every error thrown by the SDK is a `LarkError` instance with a machine-readable `code` and a human-readable `message`. ## Error structure ```typescript theme={null} import { LarkDatabase, LarkError } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); try { await db.ref("admin/secret").set("hack"); } catch (error) { if (error instanceof LarkError) { console.log(error.code); // "permission_denied" console.log(error.message); // "Permission denied at /admin/secret" } } ``` ## Error codes | Code | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `permission_denied` | Security rules rejected the operation. The authenticated user (or anonymous client) doesn't have access to the requested path or operation. | | `invalid_data` | The data you tried to write is malformed. This includes invalid key characters, values that exceed size limits, or structurally invalid data. | | `not_found` | The requested path doesn't exist. Returned by certain operations that expect existing data. | | `invalid_path` | The path string is malformed. Paths can't contain `.`, `#`, `$`, `[`, or `]` characters. | | `timeout` | The operation didn't complete within the timeout window (30 seconds by default). | | `not_connected` | You attempted an operation that requires a connection while the client is disconnected. | | `condition_failed` | A transaction's condition check failed. The data didn't match the expected state. | | `max_retries_exceeded` | A transaction was retried 25 times and still couldn't commit due to contention. | | `write_tainted` | A write was rejected because it depended on a prior write that failed. This prevents cascading inconsistencies. | | `auth_required` | The operation requires authentication, but the client is connected anonymously and security rules demand an authenticated user. | ## Handling write errors All write operations (`set`, `update`, `remove`, `push`) return promises. Wrap them in try/catch: ```typescript theme={null} try { await db.ref("users/alice/score").set(100); console.log("Write succeeded"); } catch (error) { if (error instanceof LarkError) { switch (error.code) { case "permission_denied": console.error("You don't have permission to write here"); break; case "invalid_data": console.error("Invalid data:", error.message); break; default: console.error("Write failed:", error.code, error.message); } } } ``` ## Handling read errors Read operations (`once`, `get`) can also fail: ```typescript theme={null} try { const snapshot = await db.ref("restricted/data").once("value"); console.log(snapshot.val()); } catch (error) { if (error instanceof LarkError) { console.error("Read failed:", error.code, error.message); } } ``` ## Handling transaction errors Transactions can fail for additional reasons beyond normal write errors: ```typescript theme={null} try { await db.ref("counter").transaction((current) => (current ?? 0) + 1); } catch (error) { if (error instanceof LarkError) { switch (error.code) { case "max_retries_exceeded": console.error("Too much contention, try again later"); break; case "condition_failed": console.error("Precondition not met"); break; default: console.error("Transaction failed:", error.code); } } } ``` ## Connection-level errors For errors that aren't tied to a specific operation (transport failures, authentication errors, protocol issues), use `db.onError()`: ```typescript theme={null} db.onError((error) => { console.error("Connection error:", error.code, error.message); // You might want to show a notification to the user showErrorNotification(error.message); }); ``` `db.onError()` returns an unsubscribe function, just like other event listeners. Clean it up when you no longer need it. ```typescript theme={null} const unsubscribe = db.onError((error) => { console.error(error); }); // Later unsubscribe(); ``` ## Best practices Always handle `permission_denied` errors in your UI. They usually mean a user is trying to access data they shouldn't, either a bug in your security rules or a user navigating somewhere unexpected. * Wrap writes in try/catch. Even if you're confident your security rules will allow the write, network issues or data validation can cause failures. * Log errors with their codes. The `code` field is stable and machine-readable. Use it for programmatic decisions. Use `message` for human-readable logging. * Use `db.onError()` as a safety net. It catches connection-level issues that individual operation error handling might miss. * Don't swallow errors silently. At minimum, log them. Silently dropped errors make debugging much harder. # Firebase v8 compatibility layer Source: https://docs.larksh.com/lark-sdk/fb-v8 Drop-in Firebase v8 API for the Lark SDK # Firebase v8 compatibility layer If you're migrating from Firebase Realtime Database v8 (the `firebase/database` namespaced API), the Lark SDK includes a compatibility layer that matches the Firebase v8 API surface. This lets you switch to Lark without rewriting your subscription patterns. ## Installation The compatibility layer ships as a sub-package inside `@lark-sh/client`. No extra installation needed. ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client/fb-v8"; ``` ## Key difference: `on()` and `off()` The biggest API difference between the modern Lark SDK and the Firebase v8 compatibility layer is how subscriptions work. **Modern Lark SDK**: `on()` returns an unsubscribe function: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // on() returns an unsubscribe function const unsubscribe = db.ref("scores").on("value", (snapshot) => { console.log(snapshot.val()); }); // Call it to stop listening unsubscribe(); ``` **Firebase v8 compatibility**: `on()` returns the callback, and you use `off()` to stop listening: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client/fb-v8"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // on() returns the callback you passed in const callback = db.ref("scores").on("value", (snapshot) => { console.log(snapshot.val()); }); // Use off() to stop listening — pass the event type and callback db.ref("scores").off("value", callback); ``` ## Side-by-side comparison | Operation | Modern (`@lark-sh/client`) | Firebase v8 compat (`@lark-sh/client/fb-v8`) | | ------------------------- | ----------------------------------- | -------------------------------------------- | | Subscribe | `const unsub = ref.on('value', cb)` | `const cb = ref.on('value', cb)` | | Unsubscribe | `unsub()` | `ref.off('value', cb)` | | Unsubscribe all for event | Not applicable | `ref.off('value')` | | Unsubscribe all | Not applicable | `ref.off()` | | Return value of `on()` | Unsubscribe function | The callback itself | ## Context parameter The Firebase v8 API supports a `context` parameter for binding `this` inside your callback. The compatibility layer supports this too: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client/fb-v8"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); class ScoreTracker { scores: Record = {}; startListening(ref: ReturnType) { ref.on( "child_added", function (this: ScoreTracker, snapshot) { this.scores[snapshot.key] = snapshot.val(); }, this // context parameter — binds `this` inside the callback ); } stopListening(ref: ReturnType) { ref.off("child_added"); } } ``` ## When to use the compatibility layer Use `@lark-sh/client/fb-v8` when: * You're migrating a Firebase v8 codebase and don't want to rewrite every `on()`/`off()` call pattern. * You have shared libraries or utilities that expect the Firebase v8 subscription API. * You need `off()` semantics like removing all listeners for a specific event type with a single call. Use the modern `@lark-sh/client` when: * You're starting a new project. The unsubscribe-function pattern is cleaner and less error-prone. * You're using React, Vue, or Svelte. Framework cleanup hooks work naturally with unsubscribe functions. * You want the simplest API. No need to track callback references or worry about matching the right callback in `off()`. You can use both imports in the same project during a migration. They share the same underlying connection, so you won't create duplicate connections. ## Migration path If you decide to migrate from the compatibility layer to the modern API later, the changes are mechanical: ```typescript theme={null} // Before (fb-v8) import { LarkDatabase } from "@lark-sh/client/fb-v8"; const callback = ref.on("value", (snapshot) => { // handle data }); // Later... ref.off("value", callback); ``` ```typescript theme={null} // After (modern) import { LarkDatabase } from "@lark-sh/client"; const unsubscribe = ref.on("value", (snapshot) => { // handle data }); // Later... unsubscribe(); ``` The rest of the API (`set()`, `update()`, `remove()`, `once()`, `transaction()`, queries, OnDisconnect) is identical between the two. # Offline and reconnection Source: https://docs.larksh.com/lark-sdk/offline Handle network interruptions gracefully # Offline and reconnection Network connections drop. Users go through tunnels, switch between Wi-Fi and cellular, or temporarily lose signal. The Lark SDK handles all of this automatically so your app stays functional. ## Automatic reconnection When a connection drops unexpectedly, the SDK reconnects automatically using exponential backoff with jitter: | Attempt | Delay | | ------- | ----------- | | 1 | \~1s | | 2 | \~2s | | 3 | \~4s | | 4 | \~8s | | 5 | \~16s | | 6+ | \~30s (max) | Jitter adds a random factor so that thousands of clients don't all reconnect at the same instant after a server restart. When the connection is re-established: * All active subscriptions are automatically re-registered. * Any pending writes that were queued while offline are sent to the server. * Your `onConnect` callbacks fire again. ## Manual control ### `goOffline()` and `goOnline()` Pause and resume the connection manually. This preserves your local cache and subscription registrations. ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // Pause the connection (e.g., app going to background) db.goOffline(); // Resume later db.goOnline(); ``` While offline via `goOffline()`: * Writes are queued locally and sent when you go back online. * Subscriptions still reflect the last known cached data. * No network traffic occurs. ### `disconnect()` Fully tears down the connection and clears everything: cache, subscriptions, pending writes. Use this when you're completely done with the database. ```typescript theme={null} db.disconnect(); ``` ### Comparison | | `goOffline()` / `goOnline()` | `disconnect()` | | ---------------- | ----------------------------------------- | -------------------------- | | Local cache | Preserved | Cleared | | Subscriptions | Preserved, re-established on `goOnline()` | Removed | | Pending writes | Preserved, sent on `goOnline()` | Cleared | | Reconnect method | `goOnline()` | New instance + `connect()` | | Use case | Temporary pause (background/foreground) | Permanent teardown | ## Pending writes When the SDK is offline, writes queue up locally. You can inspect and manage this queue. ### `db.hasPendingWrites()` Returns `true` if there are writes waiting to be sent to the server: ```typescript theme={null} if (db.hasPendingWrites()) { showSyncIndicator(); } ``` ### `db.getPendingWriteCount()` Returns the number of pending writes: ```typescript theme={null} const count = db.getPendingWriteCount(); console.log(`${count} writes pending`); ``` ### `db.clearPendingWrites()` Discards all pending writes. Use this carefully, because those writes will be lost permanently. ```typescript theme={null} db.clearPendingWrites(); ``` `clearPendingWrites()` cannot be undone. Any writes that haven't been acknowledged by the server are permanently discarded. Only use this if you're sure you want to throw away those changes. ## Connection status The SDK provides a special path `.info/connected` that reflects the current connection state. This is useful for showing online/offline indicators in your UI. ```typescript theme={null} const unsubscribe = db.ref(".info/connected").on("value", (snapshot) => { const connected = snapshot.val(); if (connected) { console.log("Online"); } else { console.log("Offline"); } }); ``` Combine `.info/connected` with [OnDisconnect](/lark-sdk/ondisconnect) operations to build robust presence systems that track which users are currently online. ## Monitoring reconnection Use lifecycle callbacks to track reconnection attempts: ```typescript theme={null} db.onDisconnect(() => { console.log("Connection lost"); showOfflineBanner(); }); db.onReconnecting((attempt) => { console.log(`Reconnecting... attempt ${attempt}`); }); db.onConnect(() => { console.log("Back online"); hideOfflineBanner(); }); ``` # OnDisconnect Source: https://docs.larksh.com/lark-sdk/ondisconnect Queue operations that run when a client disconnects OnDisconnect operations let you write data to the server **when the client disconnects**. The server executes these writes even if the client crashes, closes the browser tab, or loses network without a clean goodbye. This is the building block for presence systems, cleanup logic, and any scenario where you need the server to act on behalf of a client that's gone. For a conceptual overview, see [OnDisconnect and presence](/platform/presence). ## Creating OnDisconnect operations Call `ref.onDisconnect()` to get an `OnDisconnect` object, then chain a write method: ```typescript theme={null} import { LarkDatabase, ServerValue } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // When this client disconnects, set "online" to false await db.ref("users/alice/online").onDisconnect().set(false); ``` Every method returns a promise that resolves when the server has acknowledged and registered the operation. ## Methods ### `.set(value)` Sets a value when the client disconnects: ```typescript theme={null} await db.ref("users/alice/status").onDisconnect().set("offline"); ``` ### `.update(values)` Performs a shallow merge when the client disconnects: ```typescript theme={null} await db.ref("users/alice").onDisconnect().update({ online: false, lastSeen: ServerValue.TIMESTAMP, }); ``` ### `.remove()` Deletes data when the client disconnects: ```typescript theme={null} await db.ref("sessions/session-abc").onDisconnect().remove(); ``` ### `.setWithPriority(value, priority)` Sets a value with a priority when the client disconnects: ```typescript theme={null} await db.ref("queue/task-1").onDisconnect().setWithPriority("abandoned", 0); ``` ### `.cancel()` Cancels a previously registered OnDisconnect operation: ```typescript theme={null} const onDisconnectRef = db.ref("users/alice/online").onDisconnect(); // Register it await onDisconnectRef.set(false); // Changed our mind — cancel it await onDisconnectRef.cancel(); ``` ## Presence system The most common use case for OnDisconnect is a presence system: tracking which users are currently online. Here's a complete implementation: ```typescript theme={null} import { LarkDatabase, ServerValue } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { token: "user-jwt-token" }); const uid = db.auth!.uid; const userStatusRef = db.ref(`users/${uid}/status`); // Watch connection state db.ref(".info/connected").on("value", async (snapshot) => { if (!snapshot.val()) { return; // We're offline — the server will handle it } // We just connected (or reconnected). // Register what should happen when we disconnect. await userStatusRef.onDisconnect().update({ online: false, lastSeen: ServerValue.TIMESTAMP, }); // Now set ourselves as online await userStatusRef.update({ online: true, lastSeen: ServerValue.TIMESTAMP, }); }); ``` The order matters. Register the OnDisconnect operation **before** writing your online status. This avoids a race condition where you set `online: true` but the connection drops before the OnDisconnect is registered. ### Why subscribe to `.info/connected`? You might wonder why we don't just run the setup once. The reason is **reconnection**. If the client loses its connection and reconnects, previous OnDisconnect operations are cleared. By listening to `.info/connected`, you re-register the OnDisconnect handler every time the connection is re-established. ### Displaying presence Other clients can subscribe to presence data: ```typescript theme={null} // Watch a specific user's online status const unsubscribe = db.ref("users/bob/online").on("value", (snapshot) => { if (snapshot.val()) { console.log("Bob is online"); } else { console.log("Bob is offline"); } }); // Or watch all online users const unsubAll = db.ref("users") .orderByChild("status/online") .equalTo(true) .on("value", (snapshot) => { const onlineUsers: string[] = []; snapshot.forEach((child) => { onlineUsers.push(child.key); }); renderOnlineList(onlineUsers); }); ``` ## Cleanup on disconnect Beyond presence, OnDisconnect is useful for any cleanup that should happen when a client leaves: ```typescript theme={null} // Remove a player from a game lobby await db.ref(`lobbies/lobby-1/players/${uid}`).onDisconnect().remove(); // Release a lock await db.ref(`locks/document-42`).onDisconnect().remove(); // Mark a task as abandoned await db.ref(`tasks/task-99/assignee`).onDisconnect().set(null); ``` OnDisconnect operations run on the server, so they're limited to simple write operations (`set`, `update`, `remove`). You can't run transactions or custom logic in an OnDisconnect handler. # Lark SDK overview Source: https://docs.larksh.com/lark-sdk/overview The native JavaScript/TypeScript SDK for Lark # Lark SDK The Lark SDK (`@lark-sh/client`) is the official JavaScript/TypeScript client for Lark databases. It gives you real-time data synchronization, offline support, and a modern developer experience in a lightweight package. ## Lark SDK Features The entire SDK is \~20KB gzipped. Subscriptions return an unsubscribe function directly, so there's no need to track callbacks or call a separate `off()` method. This plays nicely with React's `useEffect` cleanup, Vue's `onUnmounted`, and any other framework lifecycle pattern. ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // on() returns an unsubscribe function — no need to await connect() first const unsubscribe = db.ref("scores").on("value", (snapshot) => { console.log(snapshot.val()); }); // Clean up when you're done unsubscribe(); ``` The SDK connects over WebSocket by default. You can opt in to WebTransport (HTTP/3) for lower latency and better multiplexing in supported browsers by setting `transport: 'auto'`. Ephemeral data like cursor positions, typing indicators, and player coordinates can flow through volatile paths, which are high-frequency updates that skip persistence for maximum speed. Every method, option, and return value is fully typed with TypeScript. ## Installation ```bash theme={null} npm install @lark-sh/client ``` Or with your preferred package manager: ```bash theme={null} yarn add @lark-sh/client pnpm add @lark-sh/client ``` ## Platform support The Lark SDK works in **browsers** and **Node.js**. Both CommonJS and ESM builds are included, and your bundler or runtime will pick the right one automatically. | Platform | Support | | ----------------------------------------------- | ------------ | | Modern browsers (Chrome, Firefox, Safari, Edge) | Full support | | Node.js 18+ | Full support | | React Native | Full support | | Deno | ESM import | ## Package Everything you need is in a single package: ```typescript theme={null} import { LarkDatabase, ServerValue } from "@lark-sh/client"; ``` If you're migrating from Firebase v8, the SDK also ships a compatibility layer at `@lark-sh/client/fb-v8`. See the [Firebase v8 compatibility page](/lark-sdk/fb-v8) for details. ## What's next Connect, write, read, and subscribe in under five minutes. Connection options, transports, and lifecycle events. Set, update, remove, push, and multi-path writes. Listen for real-time changes with different event types. # Queries Source: https://docs.larksh.com/lark-sdk/queries Sort, filter, and paginate data with chainable queries # Queries Queries let you sort, filter, and limit the data returned from a reference. All query methods are chainable and return a new query. They don't modify the original reference. ## Ordering Every query starts with an ordering method. You can only use **one** `orderBy` per query. ### `orderByChild(path)` Sort by a child key's value: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // Sort players by score const topPlayers = db.ref("players").orderByChild("score"); ``` You can also order by nested child paths: ```typescript theme={null} // Sort by a nested field const byCity = db.ref("users").orderByChild("address/city"); ``` ### `orderByKey()` Sort by each child's key (alphabetically): ```typescript theme={null} const alphabetical = db.ref("users").orderByKey(); ``` ### `orderByValue()` Sort by each child's value directly. Useful when children are primitives (numbers, strings) rather than objects: ```typescript theme={null} const byScore = db.ref("highscores").orderByValue(); ``` ### `orderByPriority()` Sort by the priority set on each child node: ```typescript theme={null} const byPriority = db.ref("tasks").orderByPriority(); ``` ## Limiting results ### `limitToFirst(count)` Returns only the first `count` items in the ordered result: ```typescript theme={null} // Get the 10 lowest scores const bottom10 = db.ref("players") .orderByChild("score") .limitToFirst(10); ``` ### `limitToLast(count)` Returns only the last `count` items in the ordered result: ```typescript theme={null} // Get the 10 highest scores const top10 = db.ref("players") .orderByChild("score") .limitToLast(10); ``` ## Range filters Range methods narrow results to a specific window. They work with the active ordering. ### `startAt(value, key?)` Include items with a value greater than or equal to the specified value: ```typescript theme={null} // Players with score >= 100 const eliteRef = db.ref("players") .orderByChild("score") .startAt(100); ``` ### `startAfter(value, key?)` Include items with a value strictly greater than the specified value: ```typescript theme={null} // Players with score > 100 const aboveRef = db.ref("players") .orderByChild("score") .startAfter(100); ``` ### `endAt(value, key?)` Include items with a value less than or equal to the specified value: ```typescript theme={null} // Players with score <= 50 const belowRef = db.ref("players") .orderByChild("score") .endAt(50); ``` ### `endBefore(value, key?)` Include items with a value strictly less than the specified value: ```typescript theme={null} // Players with score < 50 const underRef = db.ref("players") .orderByChild("score") .endBefore(50); ``` ### `equalTo(value, key?)` Match items with exactly the specified value: ```typescript theme={null} // Players with score of exactly 100 const exact = db.ref("players") .orderByChild("score") .equalTo(100); ``` The optional `key` parameter in range methods is used to disambiguate when multiple children have the same value. It acts as a secondary sort by key. ## Query identifier Each query has a `queryIdentifier` property, a string that uniquely identifies the combination of ordering, limits, and ranges. This is useful for deduplication or caching: ```typescript theme={null} const query = db.ref("players") .orderByChild("score") .limitToLast(10); console.log(query.queryIdentifier); // Unique string representing this exact query configuration ``` ## Using queries with subscriptions Queries work with both `once()` and `on()`. This is where they really shine: you can subscribe to a filtered, sorted slice of your data in real time. ```typescript theme={null} // Subscribe to top 10 players, updated live const unsubscribe = db.ref("players") .orderByChild("score") .limitToLast(10) .on("child_added", (snapshot) => { console.log(`${snapshot.val().name}: ${snapshot.val().score}`); }); ``` ## Examples ### Leaderboard Display a live top-10 leaderboard sorted by score: ```typescript theme={null} const leaderboard: Array<{ name: string; score: number }> = []; const unsubscribe = db.ref("players") .orderByChild("score") .limitToLast(10) .on("value", (snapshot) => { leaderboard.length = 0; snapshot.forEach((childSnap) => { leaderboard.push({ name: childSnap.val().name, score: childSnap.val().score, }); }); // Reverse because limitToLast returns ascending order leaderboard.reverse(); console.log("Top 10:", leaderboard); }); ``` ### Pagination Load data page by page using `startAfter()` and `limitToFirst()`: ```typescript theme={null} async function loadPage( ref: ReturnType, pageSize: number, lastKey?: string ) { let query = ref.orderByKey().limitToFirst(pageSize); if (lastKey) { query = query.startAfter(lastKey); } const snapshot = await query.once("value"); const items: Array<{ key: string; value: any }> = []; snapshot.forEach((child) => { items.push({ key: child.key, value: child.val() }); }); return items; } // Load first page const page1 = await loadPage(db.ref("products"), 20); // Load next page, starting after the last key from page 1 const lastKey = page1[page1.length - 1]?.key; const page2 = await loadPage(db.ref("products"), 20, lastKey); ``` You can only use one `orderBy` method per query. Calling a second `orderBy` will throw an error. If you need to filter on multiple fields, restructure your data so a single ordering covers your use case, or filter client-side after fetching. # Quickstart Source: https://docs.larksh.com/lark-sdk/quickstart Connect to Lark and start reading and writing data in minutes # Quickstart This guide takes you from zero to a working Lark connection in under five minutes. You'll connect to a database, write data, read it back, and subscribe to real-time updates. ## 1. Install the SDK ```bash theme={null} npm install @lark-sh/client ``` ## 2. Connect to your database ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); ``` That's it. Operations queue automatically and run once the connection is established. No need to `await` anything before you start using the database. You can find your project and database IDs in the Lark dashboard. The format is always `projectId/databaseId`. ## 3. Write data ```typescript theme={null} // Set data at a specific path await db.ref("users/alice").set({ name: "Alice", score: 42, online: true, }); ``` ## 4. Read data once ```typescript theme={null} // Read the data you just wrote const snapshot = await db.ref("users/alice").once("value"); console.log(snapshot.val()); // { name: "Alice", score: 42, online: true } ``` ## 5. Subscribe to real-time updates ```typescript theme={null} // Listen for changes — on() returns an unsubscribe function const unsubscribe = db.ref("users/alice/score").on("value", (snapshot) => { console.log("Score changed:", snapshot.val()); }); // Update the score from somewhere else — your listener fires automatically await db.ref("users/alice/score").set(99); // Console: "Score changed: 99" ``` ## 6. Clean up ```typescript theme={null} // Stop listening unsubscribe(); // Disconnect when you're done db.disconnect(); ``` ## Full working example Here's everything in one file: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; async function main() { const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // Write await db.ref("users/alice").set({ name: "Alice", score: 42, online: true, }); // Read once const snapshot = await db.ref("users/alice").once("value"); console.log("Read:", snapshot.val()); // Subscribe to real-time updates const unsubscribe = db.ref("users/alice/score").on("value", (snap) => { console.log("Score is now:", snap.val()); }); // Make a change — the subscription fires await db.ref("users/alice/score").set(100); // Clean up unsubscribe(); db.disconnect(); } main(); ``` ## Next steps * [Connecting](/lark-sdk/connecting): connection options, transport selection, and lifecycle events * [Writing data](/lark-sdk/writing): set, update, remove, push, and multi-path writes * [Subscriptions](/lark-sdk/subscriptions): listen for real-time changes with different event types * [Authentication](/lark-sdk/auth): move beyond anonymous auth with tokens # Reading data Source: https://docs.larksh.com/lark-sdk/reading Read data from your Lark database # Reading data Sometimes you just need to fetch data once without subscribing to ongoing changes. The Lark SDK provides two equivalent methods for this. ## `once(eventType)` Reads data at a reference and returns a promise that resolves with a `DataSnapshot`: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); const snapshot = await db.ref("users/alice").once("value"); console.log(snapshot.val()); // { name: "Alice", score: 42, online: true } ``` ## `get()` An alias for `once('value')`. Same behavior, shorter syntax: ```typescript theme={null} const snapshot = await db.ref("users/alice").get(); console.log(snapshot.val()); // { name: "Alice", score: 42, online: true } ``` ## Working with snapshots Both methods return a `DataSnapshot`. Here are the key properties and methods: ### `snapshot.val()` Returns the data as a JavaScript value (object, array, string, number, boolean, or `null`): ```typescript theme={null} const snapshot = await db.ref("users/alice").get(); const user = snapshot.val(); // { name: "Alice", score: 42 } ``` ### `snapshot.exists()` Returns `true` if data exists at this location, `false` if it's `null`: ```typescript theme={null} const snapshot = await db.ref("users/nonexistent").get(); if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data here"); } ``` ### `snapshot.key` The key (last path segment) of the location this snapshot represents: ```typescript theme={null} const snapshot = await db.ref("users/alice").get(); console.log(snapshot.key); // "alice" ``` If you need data that stays up to date as it changes, use [real-time subscriptions](/lark-sdk/subscriptions) instead. `once()` and `get()` fetch a single point-in-time snapshot and don't update afterward. # References and paths Source: https://docs.larksh.com/lark-sdk/references Navigate your data tree with database references # References and paths A **reference** points to a specific location in your database. You use references to read, write, and listen to data at that location. ## Creating a reference Call `db.ref(path)` with a slash-separated path string: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); const usersRef = db.ref("users"); const aliceRef = db.ref("users/alice"); const scoreRef = db.ref("users/alice/score"); ``` Call `db.ref()` with no arguments (or an empty string) to get a reference to the root of your database: ```typescript theme={null} const rootRef = db.ref(); ``` References are lightweight objects. Creating a reference does **not** fetch any data or open any connections. It simply represents a path. You can create as many as you need without any performance cost. ## Navigating with references You can move around the data tree from any reference. ### `.child(path)` Returns a reference to a child location: ```typescript theme={null} const usersRef = db.ref("users"); const aliceRef = usersRef.child("alice"); const scoreRef = aliceRef.child("score"); // Equivalent to: const sameScoreRef = db.ref("users/alice/score"); ``` You can also pass deeper paths: ```typescript theme={null} const scoreRef = db.ref("users").child("alice/score"); ``` ### `.parent` Returns a reference to the parent location. Returns `null` for the root reference. ```typescript theme={null} const scoreRef = db.ref("users/alice/score"); const aliceRef = scoreRef.parent; // "users/alice" const usersRef = aliceRef.parent; // "users" const rootRef = usersRef.parent; // root const nothing = rootRef.parent; // null ``` ### `.root` Returns a reference to the root of the database, no matter where you start: ```typescript theme={null} const deepRef = db.ref("a/b/c/d/e"); const rootRef = deepRef.root; // root ``` ## Properties ### `.key` The last segment of the path. For root references, this is `null`. ```typescript theme={null} db.ref("users/alice/score").key; // "score" db.ref("users/alice").key; // "alice" db.ref("users").key; // "users" db.ref().key; // null ``` ### `.path` The full path string from the root: ```typescript theme={null} db.ref("users/alice/score").path; // "users/alice/score" db.ref().path; // "" ``` ## Practical example References make it easy to work with related data without repeating path strings: ```typescript theme={null} const gameRef = db.ref("games/game-123"); // Navigate to different parts of the game const playersRef = gameRef.child("players"); const settingsRef = gameRef.child("settings"); const chatRef = gameRef.child("chat"); // Write to multiple locations await settingsRef.set({ maxPlayers: 4, timeLimit: 300 }); await playersRef.child("player-1").set({ name: "Alice", ready: true }); // Read from a sibling path const settings = await settingsRef.once("value"); console.log(settings.val().maxPlayers); // 4 // Navigate back up const sameGameRef = playersRef.parent; // back to "games/game-123" console.log(sameGameRef.key); // "game-123" ``` Store references in variables when you use the same path repeatedly. It keeps your code cleaner and avoids typos in path strings. # Subscriptions Source: https://docs.larksh.com/lark-sdk/subscriptions Listen for real-time updates with the Lark SDK Subscriptions are the core of Lark's real-time capabilities. When data changes on the server, your callback fires with the updated data. For a conceptual overview of how subscriptions work under the hood (delta sync, shared views), see [Subscriptions](/platform/subscriptions). ## Basic usage Call `ref.on(eventType, callback)` to start listening. It returns an **unsubscribe function** you call when you're done: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); const unsubscribe = db.ref("scores/player1").on("value", (snapshot) => { console.log("Current score:", snapshot.val()); }); // Later, stop listening unsubscribe(); ``` The unsubscribe pattern works well with React's `useEffect` cleanup, Vue's `onUnmounted`, and similar framework lifecycle hooks. ## Event types ### `value` Fires once with the initial data, then again every time anything at the location (or its children) changes. The callback receives the complete snapshot. ```typescript theme={null} const unsubscribe = db.ref("game/state").on("value", (snapshot) => { const state = snapshot.val(); console.log("Full game state:", state); }); ``` ### `child_added` Fires once for each existing child, then again whenever a new child is added. The callback receives the child's snapshot and the key of the previous child (for ordering). ```typescript theme={null} const unsubscribe = db.ref("chat/messages").on( "child_added", (snapshot, prevChildKey) => { console.log("New message:", snapshot.key, snapshot.val()); console.log("After:", prevChildKey); } ); ``` ### `child_changed` Fires when an existing child's value changes. Does not fire for additions or removals. ```typescript theme={null} const unsubscribe = db.ref("players").on( "child_changed", (snapshot, prevChildKey) => { console.log("Player updated:", snapshot.key, snapshot.val()); } ); ``` ### `child_removed` Fires when a child is removed. ```typescript theme={null} const unsubscribe = db.ref("players").on("child_removed", (snapshot) => { console.log("Player left:", snapshot.key); }); ``` ### `child_moved` Fires when a child's sort order changes (due to priority or value changes when using ordered queries). ```typescript theme={null} const unsubscribe = db.ref("leaderboard") .orderByChild("score") .on("child_moved", (snapshot, prevChildKey) => { console.log("Rank changed:", snapshot.key, "now after:", prevChildKey); }); ``` ## Practical examples ### Player list Keep a live roster of players in a multiplayer game: ```typescript theme={null} const players = new Map(); const unsubAdded = db.ref("game/players").on("child_added", (snapshot) => { players.set(snapshot.key, snapshot.val()); console.log(`${snapshot.val().name} joined. Total: ${players.size}`); }); const unsubRemoved = db.ref("game/players").on("child_removed", (snapshot) => { players.delete(snapshot.key); console.log(`${snapshot.val().name} left. Total: ${players.size}`); }); const unsubChanged = db.ref("game/players").on("child_changed", (snapshot) => { players.set(snapshot.key, snapshot.val()); console.log(`${snapshot.val().name} updated.`); }); // Clean up all listeners function cleanup() { unsubAdded(); unsubRemoved(); unsubChanged(); } ``` ### Chat messages Listen for messages added to a chat room: ```typescript theme={null} const unsubscribe = db.ref("rooms/lobby/messages") .orderByChild("timestamp") .limitToLast(50) .on("child_added", (snapshot) => { const msg = snapshot.val(); displayMessage(msg.sender, msg.text, msg.timestamp); }); ``` ### Live game score Track a single value that changes frequently: ```typescript theme={null} const unsubscribe = db.ref("game/score").on("value", (snapshot) => { const score = snapshot.val() ?? 0; updateScoreDisplay(score); }); ``` ### Leaderboard Watch a sorted, limited query: ```typescript theme={null} const unsubscribe = db.ref("players") .orderByChild("score") .limitToLast(10) .on("value", (snapshot) => { const leaderboard: Array<{ name: string; score: number }> = []; snapshot.forEach((child) => { leaderboard.push(child.val()); }); leaderboard.reverse(); // limitToLast returns ascending, flip for top-first renderLeaderboard(leaderboard); }); ``` ## Unsubscribing Always unsubscribe when you no longer need updates. This frees up resources on both the client and server. ```typescript theme={null} const unsubscribe = db.ref("data").on("value", callback); // Call it when done unsubscribe(); ``` Forgetting to unsubscribe is a common source of memory leaks and unexpected behavior. Always clean up your listeners when a component unmounts or a view is no longer active. ### React example ```typescript theme={null} import { useEffect, useState } from "react"; import { LarkDatabase } from "@lark-sh/client"; function ScoreDisplay({ db, playerId }: { db: LarkDatabase; playerId: string }) { const [score, setScore] = useState(0); useEffect(() => { const unsubscribe = db.ref(`scores/${playerId}`).on("value", (snapshot) => { setScore(snapshot.val() ?? 0); }); return unsubscribe; }, [db, playerId]); return
Score: {score}
; } ``` # Transactions Source: https://docs.larksh.com/lark-sdk/transactions Atomic operations with the Lark SDK Transactions let you update data based on its current value without worrying about conflicts from other clients. They're essential for counters, inventory systems, auctions, and any scenario where a write depends on the existing data. For a conceptual overview of how transactions work and why you need them, see [Transactions](/platform/transactions). ## Callback-style transactions The most common pattern. Pass a function that receives the current value and returns the new value: ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); const result = await db.ref("counters/pageViews").transaction((currentValue) => { return (currentValue ?? 0) + 1; }); console.log(result.committed); // true if the transaction succeeded console.log(result.snapshot.val()); // The final committed value ``` ### Transaction results Every `transaction()` call returns a result object: | Property | Type | Description | | ----------- | -------------- | ---------------------------------------------------------------- | | `committed` | `boolean` | `true` if the transaction wrote data, `false` if it was aborted. | | `snapshot` | `DataSnapshot` | The final value at the path after the transaction. | ### Aborting a transaction Return `undefined` from your update function to abort without writing: ```typescript theme={null} const result = await db.ref("inventory/item-1").transaction((current) => { if (current === null) { return undefined; // Item doesn't exist, abort } if (current.quantity <= 0) { return undefined; // Out of stock, abort } return { ...current, quantity: current.quantity - 1, }; }); if (!result.committed) { console.log("Transaction was aborted"); } ``` ### Retry limit Transactions automatically retry up to **25 times**. If they still can't commit after 25 attempts (due to extremely high contention), the promise rejects with a `max_retries_exceeded` error. ```typescript theme={null} try { await db.ref("hot-counter").transaction((val) => (val ?? 0) + 1); } catch (error) { if (error.code === "max_retries_exceeded") { console.error("Too much contention on this path"); } } ``` Your update function may be called **multiple times** if there are concurrent writes. Make sure it has no side effects. Don't make network requests, modify external state, or log analytics inside it. ## Multi-path transactions When you need to update multiple paths atomically, either all the changes happen or none of them do. ### Object syntax The simplest form. Pass an object where keys are paths and values are what to write. Use `null` to delete a path. ```typescript theme={null} await db.transaction({ "/players/alice/coins": 50, "/players/bob/coins": 150, "/trades/latest": { from: "alice", to: "bob", amount: 50 }, }); ``` All three writes happen atomically. If any one fails, none of them are applied. ### Array syntax with conditions For more control, use the array syntax with explicit operations. This lets you add conditions that check the current value before proceeding. ```typescript theme={null} await db.transaction([ // Only proceed if Alice has exactly 100 coins { op: "condition", path: "/players/alice/coins", value: 100 }, // Transfer 50 coins { op: "set", path: "/players/alice/coins", value: 50 }, { op: "set", path: "/players/bob/coins", value: 150 }, ]); ``` If any condition fails, the entire transaction is rejected and no writes are applied. You can also pass a snapshot of a complex object as a condition. Internally, the LarkJS library will compute a hash representing the state of this object and pass it as the condition to the server, keeping the transaction efficient (so the entire object isn't sent). ```typescript theme={null} const snapshot = await db.ref("/game/state").once("value"); await db.transaction([ { op: "condition", path: "/game/state", value: snapshot }, { op: "set", path: "/game/state", value: newState }, ]); ``` ## Examples ### Increment a counter ```typescript theme={null} await db.ref("stats/totalGames").transaction((current) => { return (current ?? 0) + 1; }); ``` ### Update a high score (only if higher) ```typescript theme={null} const newScore = 250; const result = await db.ref("players/alice/highScore").transaction((current) => { if (current !== null && current >= newScore) { return undefined; // Current high score is already higher, abort } return newScore; }); if (result.committed) { console.log("New high score recorded:", result.snapshot.val()); } ``` ### Transfer currency between players ```typescript theme={null} // Read current values first const aliceSnapshot = await db.ref("players/alice/coins").once("value"); const bobSnapshot = await db.ref("players/bob/coins").once("value"); const aliceCoins = aliceSnapshot.val(); const bobCoins = bobSnapshot.val(); const transferAmount = 50; // Use a conditional multi-path transaction await db.transaction([ // Ensure neither balance changed since we read it { op: "condition", path: "/players/alice/coins", value: aliceCoins }, { op: "condition", path: "/players/bob/coins", value: bobCoins }, // Apply the transfer { op: "set", path: "/players/alice/coins", value: aliceCoins - transferAmount }, { op: "set", path: "/players/bob/coins", value: bobCoins + transferAmount }, ]); ``` ### Claim a unique resource Use a transaction to ensure only one client can claim something: ```typescript theme={null} const result = await db.ref("game/crown").transaction((current) => { if (current !== null) { return undefined; // Someone already claimed it } return { claimedBy: "alice", claimedAt: Date.now() }; }); if (result.committed) { console.log("Crown claimed!"); } else { console.log("Someone else got it first"); } ``` ### Conditional multi-path update Only claim a reward if it hasn't been claimed yet: ```typescript theme={null} await db.transaction([ { op: "condition", path: "/rewards/reward1/claimed", value: false }, { op: "set", path: "/rewards/reward1/claimed", value: true }, { op: "set", path: "/rewards/reward1/claimedBy", value: "alice" }, ]); ``` Avoid running transactions on paths with very high write contention from many clients simultaneously. If you're hitting the retry limit frequently, consider restructuring your data to reduce contention. For example, you could shard counters across multiple paths. # Writing data Source: https://docs.larksh.com/lark-sdk/writing Set, update, remove, and push data with the Lark SDK # Writing data The Lark SDK gives you several ways to write data. All write operations are **optimistic**: your local state updates immediately, and the write is sent to the server in the background. If the server rejects it (due to security rules, for example), the local state rolls back. ## `set(value)` Overwrites the data at a reference. Any existing data at that location, including all children, is replaced. ```typescript theme={null} import { LarkDatabase } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); // Write an object await db.ref("users/alice").set({ name: "Alice", score: 42, level: 3, }); // Write a primitive value await db.ref("users/alice/score").set(100); // Write null to delete the data (same as remove) await db.ref("users/alice").set(null); ``` `set()` replaces everything at that path. If you only want to update specific fields without touching others, use `update()` instead. ## `update(values)` Performs a shallow merge at the reference location. Only the specified keys are written. Other existing keys are left untouched. ```typescript theme={null} // Only updates "score" and "level", leaves "name" unchanged await db.ref("users/alice").update({ score: 99, level: 5, }); ``` ### Multi-path updates You can update multiple locations atomically by calling `update()` on a parent reference with full paths as keys: ```typescript theme={null} await db.ref().update({ "users/alice/score": 99, "leaderboard/alice": 99, "stats/totalGames": 150, }); ``` This is powerful for keeping denormalized data in sync. Either all paths update or none do. Multi-path updates are one of the most useful patterns in Lark. Use them any time you need to write to several locations as a single atomic operation. ## `remove()` Deletes data at a reference. Equivalent to `set(null)`. ```typescript theme={null} await db.ref("users/alice").remove(); ``` ## `push(value)` Generates a unique key and writes the value under it. Returns a reference to the new child location. ```typescript theme={null} const messagesRef = db.ref("chat/messages"); const newMessageRef = messagesRef.push({ text: "Hello!", sender: "alice", timestamp: Date.now(), }); console.log(newMessageRef.key); // Something like "-NxKj2a..." ``` You can also call `push()` without a value to just generate the key: ```typescript theme={null} const newRef = messagesRef.push(); console.log(newRef.key); // Generated key // Write to it later await newRef.set({ text: "Delayed message", sender: "bob", }); ``` ## `setWithPriority(value, priority)` Sets data along with a priority value. Priorities affect the default sort order when querying. ```typescript theme={null} await db.ref("tasks/task-1").setWithPriority( { title: "Buy groceries", done: false }, 1 ); await db.ref("tasks/task-2").setWithPriority( { title: "Walk the dog", done: false }, 2 ); ``` ## `setPriority(priority)` Updates only the priority of an existing node without changing its value. ```typescript theme={null} await db.ref("tasks/task-1").setPriority(10); ``` ## Server values The SDK provides special server-side values through `ServerValue`. ### `ServerValue.TIMESTAMP` Resolves to the server's current timestamp (milliseconds since epoch) when the write is processed. ```typescript theme={null} import { LarkDatabase, ServerValue } from "@lark-sh/client"; const db = new LarkDatabase("my-project/my-database", { anonymous: true }); await db.ref("users/alice").update({ lastSeen: ServerValue.TIMESTAMP, }); ``` `ServerValue.TIMESTAMP` is evaluated on the server, so it reflects the server's clock, not the client's. This is important for consistency across clients in different time zones or with inaccurate local clocks. ## Write guarantees All writes return a promise. The promise resolves when the server has **acknowledged** the write. If the write is rejected (e.g., by security rules), the promise rejects with a `LarkError`. ```typescript theme={null} try { await db.ref("admin/secret").set("top-secret"); } catch (error) { console.error(error.code); // "permission_denied" console.error(error.message); } ``` Even though writes are optimistic (local state updates immediately), you should still handle errors to catch permission issues and data validation failures. # Authentication Source: https://docs.larksh.com/platform/authentication How identity and access control work in Lark Authentication tells Lark who is making a request. Once a client is authenticated, their identity is available in [security rules](/platform/security-rules) as the `auth` variable. This is how you control who can read and write what. ## Anonymous access The simplest way to connect. No credentials, no tokens. The client connects and starts reading and writing immediately. In security rules, `auth` will be `null` for anonymous clients. You can allow anonymous access where appropriate: ```json theme={null} { "rules": { "public": { ".read": true, ".write": true }, "private": { ".read": "auth !== null", ".write": "auth !== null" } } } ``` Anonymous access is useful for public data, read-only content, or getting started quickly during development. For anything user-specific or sensitive, you'll want authenticated connections. Anonymous access means anyone can connect. If your security rules grant write access to anonymous users, anyone can modify that data. Use anonymous access intentionally, not as a default. ## Lark tokens Lark tokens are JWTs (JSON Web Tokens) signed with your project's secret key using HS256. You generate them on your backend server and pass them to the client. ### Generating a token On your server, create a JWT with at least a `uid` field: ```typescript theme={null} import jwt from 'jsonwebtoken'; const SECRET_KEY = process.env.LARK_SECRET_KEY; function createLarkToken(uid: string, claims?: Record) { const payload = { uid, ...claims }; return jwt.sign(payload, SECRET_KEY, { algorithm: 'HS256', expiresIn: '24h' }); } // Generate a token for Alice with a role claim const token = createLarkToken('alice', { role: 'admin', teamId: 'team-42' }); ``` ### The `auth` object in security rules Once authenticated, the `auth` object in security rules contains the token's payload: * `auth.uid`: The user's unique ID from the `uid` field in the token. * `auth.token`: The full token payload, including any custom claims. ## Custom claims Include any extra data in your token to make it available in security rules. Common use cases: roles, permissions, team membership, subscription tier. ```typescript theme={null} // Server-side: generate a token with custom claims const token = createLarkToken('alice', { role: 'moderator', teamId: 'team-42', plan: 'pro' }); ``` Then use those claims in your security rules: ```json theme={null} { "rules": { "teams": { "$teamId": { ".read": "auth.token.teamId === $teamId", ".write": "auth.token.role === 'admin' && auth.token.teamId === $teamId" } }, "moderation": { ".write": "auth.token.role === 'moderator' || auth.token.role === 'admin'" } } } ``` Keep tokens small. Include the claims you need for security rules, but don't stuff entire user profiles into them. The token is sent with every connection, so smaller is better. ## Token flow Here's the full authentication flow: 1. The user logs in to your app through whatever auth system you use (email/password, OAuth, SSO). 2. Your backend server verifies the user's identity. 3. Your backend generates a signed Lark JWT containing the user's `uid` and any custom claims. 4. Your backend sends the token to the client. 5. The client passes the token to Lark on connect (or via `signIn`). 6. Lark validates the JWT signature against your project's secret key. 7. If valid, the `auth` object is populated for all security rule evaluations on this connection. Lark doesn't manage user accounts or passwords. It trusts whatever identity your backend puts in the JWT. This gives you full flexibility: use any auth provider, any user store, any login flow you want. ## Secret key management Your project's secret key is available in the [Lark dashboard](https://dashboard.lark.sh) under **Project Settings > Secret Key**. A few important rules: * Keep it on your server. Never include the secret key in client-side code, mobile apps, or anywhere a user could extract it. * Use environment variables. Store it as `LARK_SECRET_KEY` or similar, not hardcoded in source files. * Regenerate if compromised. You can regenerate your secret key from the dashboard at any time. This immediately invalidates all existing tokens, so connected clients will need to re-authenticate with tokens signed by the new key. If your secret key leaks, anyone can generate valid tokens with any `uid` and any claims. They would have full access to your database as any user. Regenerate the key immediately if you suspect it's been exposed. ## Firebase Auth compatibility If you're using Lark with Firebase SDKs, Lark also accepts Firebase Auth tokens. Your existing Firebase authentication flow works without changes. See the [Firebase Auth guide](/firebase/auth) for details on configuring this. ## Next steps Connect, sign in, sign out, and listen for auth changes with the Lark SDK. Pass auth tokens in REST API requests. # Data structure Source: https://docs.larksh.com/platform/data-structure How Lark organizes your data as a JSON tree Everything in Lark is a JSON tree. Your entire database is one big nested JSON object, and every piece of data lives at a specific path within it. ## Projects and databases Your data is organized into two levels: * **Project**: A container for your app. You create projects in the [dashboard](https://dashboard.lark.sh). Each project has a unique ID, security rules, and connection settings. * **Database**: A single JSON tree inside a project. A project can have many databases. You might use one database per game room, per user session, or just one for your whole app. When you connect, you specify both: ```typescript theme={null} const db = new LarkDatabase('my-project/my-database'); ``` ## The JSON tree Your database is a single JSON document. Here's what one might look like: ```json theme={null} { "players": { "alice": { "name": "Alice", "score": 250, "online": true }, "bob": { "name": "Bob", "score": 180, "online": false } }, "settings": { "maxPlayers": 10, "gameMode": "capture-the-flag" } } ``` Every value in this tree has a path. The path `/players/alice/score` points to the number `250`. The path `/settings/gameMode` points to the string `"capture-the-flag"`. The path `/players` points to the entire object containing Alice and Bob. ## Paths Paths are forward-slash separated strings. Each segment between slashes is a key. | Path | Points to | | ---------------------- | ------------------------ | | `/` | The root of the database | | `/players` | The `players` object | | `/players/alice` | Alice's entire object | | `/players/alice/score` | The number `250` | | `/settings/maxPlayers` | The number `10` | Paths always start from the root. There's no concept of a "relative" path in the database itself, though SDKs let you navigate from one reference to another using methods like `child()` and `parent`. ## Keys Each segment in a path is a key. Keys are the strings that identify children within a parent object. Keys have a few rules: * Maximum **768 UTF-8 bytes** in length. * Cannot contain any of these characters: `.` `$` `#` `[` `]` `/` * Cannot contain ASCII control characters (0x00–0x1F, 0x7F). * Keys starting with `.` are reserved for Lark's internal use (like `.priority` and `.value`). Key validation is strict. If you try to write data with an invalid key, the write will be rejected. Watch out for characters like `.` and `$` that are common in other systems but forbidden here. ## References A reference is a pointer to a specific path in your database. You don't read or write data directly. Instead, you create a reference to a path, then perform operations on it. ```typescript theme={null} // Create a reference to /players/alice const aliceRef = db.ref('players/alice'); // Read the data at that path const snapshot = await aliceRef.once('value'); console.log(snapshot.val()); // { name: 'Alice', score: 250, online: true } // Write to that path await aliceRef.set({ name: 'Alice', score: 300, online: true }); // Subscribe to changes at that path aliceRef.on('value', (snapshot) => { console.log('Alice changed:', snapshot.val()); }); ``` References are lightweight. Creating one doesn't fetch data or open a connection; it just records which path you want to interact with. You can navigate from one reference to another: ```typescript theme={null} const playersRef = db.ref('players'); const aliceRef = playersRef.child('alice'); const scoreRef = aliceRef.child('score'); console.log(scoreRef.path); // 'players/alice/score' ``` ## Schemaless by design Lark doesn't enforce a schema. You write JSON, Lark stores it. Any valid JSON value (strings, numbers, booleans, objects, or null) can live at any path. This makes it fast to prototype and iterate. You don't need to define tables, run migrations, or update schemas. Just write the data you need, where you need it. Just because Lark is schemaless doesn't mean your data should be chaotic. Use [security rules](/platform/security-rules) with `.validate` to enforce structure on writes: required fields, type checking, value ranges. You get the flexibility of schemaless storage with the safety of validation. ## What's next Core operations: set, update, remove, push, and multi-path updates. Control who can read and write your data. # Limits Source: https://docs.larksh.com/platform/limits Size limits, key rules, and operational constraints ## Data tree ### Keys Keys are the path segments that identify children in the [JSON tree](/platform/data-structure). | Constraint | Limit | | -------------------- | ----------------------------------------------------------------------------------- | | Max length | **768 UTF-8 bytes** | | Forbidden characters | `.` `$` `#` `[` `]` `/` newlines | | Control characters | ASCII 0x00–0x1F and 0x7F are not allowed | | Reserved prefix | Keys starting with `.` are reserved for internal use (`.priority`, `.value`, `.sv`) | | Max depth | **32 levels** (each path must be fewer than 32 levels deep) | Characters like `.` and `$` are common in many systems (email addresses, MongoDB field names, template strings) but are forbidden in Lark keys. If you need to store an email as a key, encode it first. For example, replace `.` with `,` or use a hash. ### Values | Constraint | Limit | | --------------- | ------------------------- | | Max string size | **10 MB** (UTF-8 encoded) | ### Data types Lark stores JSON. The supported value types are: | Type | Example | | ------- | ------------------------- | | String | `"hello"` | | Number | `42`, `3.14`, `-1` | | Boolean | `true`, `false` | | Object | `{ "key": "value" }` | | Null | `null` (deletes the node) | Writing `null` to a path deletes it. There's no separate "delete" wire operation; a null write and a remove are the same thing. #### A note on arrays Lark doesn't have a native array type. If you write a JSON array, Lark converts it to an object with integer keys: ```typescript theme={null} // You write this: await db.ref('items').set(['apple', 'banana', 'cherry']); // Lark stores this: // { "0": "apple", "1": "banana", "2": "cherry" } ``` This works, but it's fragile. If two clients modify the array at the same time, they'll overwrite each other's changes. For lists that multiple clients might modify, use `push` to generate unique keys instead. ## Reads | Constraint | Limit | | ------------------------------------ | --------------------------- | | Max single response size | **256 MB** | | Max nodes in a listened/queried path | **75 million** (cumulative) | | Max query duration | **15 minutes** | If you need to read data larger than 256 MB at a single location, paginate with a query or use shallow reads. For paths with more than 75 million nodes, listen to or query more specific child paths instead of the entire subtree. ## Writes | Constraint | Limit | | ---------------------------- | ------------------------------------ | | Write rate | **1,000 writes/second** per database | | Volatile write rate | **No limit** | | Max write payload (SDK) | **16 MB** (JSON-encoded) | | Max write payload (REST API) | **256 MB** | | Bytes written | **64 MB/minute** per database | | Volatile write max size | **2 KB** | The write payload limit applies to the entire JSON body of a single write operation, including `set`, `update`, and `push`. Multi-path updates are subject to the same limit across all paths combined. The 16 MB limit is on the JSON-encoded payload, not the in-memory size. JSON encoding adds overhead for keys, quotes, and structural characters, so the effective data limit is somewhat less than 16 MB. ### Volatile exceptions Volatile paths have **no rate limit** on writes per second or events per second. The 1,000 writes/second and 64 MB/minute limits apply only to regular (persisted) writes. The 2 KB per-write size limit still applies to volatile writes. ## Connections | Constraint | Limit | | ------------------------ | ------------------------ | | Simultaneous connections | **200,000** per database | A simultaneous connection is one mobile device, browser tab, or server app connected to the database. This isn't the same as your total user count, since users don't all connect at once. An app with millions of monthly users typically has far fewer simultaneous connections. To scale beyond 200,000 connections, use multiple databases. ## Summary | Category | Constraint | Limit | | ----------- | ------------------------- | ------------------------------------- | | Keys | Max length | 768 UTF-8 bytes | | Keys | Forbidden characters | `. $ # [ ] /` newlines, control chars | | Keys | Max depth | 32 levels | | Values | Max string size | 10 MB | | Reads | Max response size | 256 MB | | Reads | Max nodes in queried path | 75 million | | Reads | Max query duration | 15 minutes | | Writes | Write rate | 1,000/sec per database | | Writes | Max payload (SDK) | 16 MB | | Writes | Max payload (REST) | 256 MB | | Writes | Bytes written | 64 MB/min per database | | Writes | Volatile max size | 2 KB | | Writes | Volatile rate limit | None | | Connections | Simultaneous | 200,000 per database | ## What's next High-frequency data with no rate limits. # OnDisconnect and presence Source: https://docs.larksh.com/platform/presence Detect when users go offline and clean up automatically Knowing when users are online and offline is essential for multiplayer games, collaborative tools, and social apps. Lark provides server-side disconnect handling and a connection state indicator so you can build reliable presence systems. ## OnDisconnect OnDisconnect lets you register operations that run on the server the moment a client disconnects. You set them up while connected, and the server holds onto them. When the connection drops (whether the user closes the tab, the network fails, or the device crashes), the server executes them immediately. ### Available operations OnDisconnect supports the same write operations you use elsewhere: * `set`: Overwrite the data at the path. * `update`: Merge values into the existing data. * `remove`: Delete the data at the path. * `cancel`: Cancel any previously queued OnDisconnect operations for this path. OnDisconnect operations survive brief network interruptions. The server only executes them when it confirms the connection is truly gone, not on every momentary blip. If the client reconnects before the server times out the connection, the disconnect operations remain queued and are not fired. ## `.info/connected` Lark provides a special read-only path, `.info/connected`, that tells you whether the client is currently connected to the server. It's `true` when connected and `false` when disconnected. Subscribe to it to show a connection indicator in your UI, like a banner or "reconnecting..." message. ## Building a presence system The classic pattern for presence combines OnDisconnect, `.info/connected`, and regular writes: 1. Subscribe to `.info/connected`. 2. When the connection is established (or re-established), register an OnDisconnect to set the user offline and write a `lastSeen` timestamp. 3. Once the OnDisconnect is confirmed queued on the server, set the user online. 4. If the connection drops, the server executes the OnDisconnect, setting the user offline. 5. When the client reconnects, the cycle repeats. The order matters. Always register your OnDisconnect **before** setting the online status. If you did it the other way around and the connection dropped between the two operations, the user would appear online forever with no disconnect handler to clean up. ### Multiple connections per user OnDisconnect operations are tied to a specific connection. If a user opens your app in two tabs, each tab has its own connection and its own OnDisconnect handlers. Closing one tab fires that tab's disconnect operations, which could set the user offline while they're still active in the other tab. Design your presence data model to handle this if needed (for example, by tracking connection count instead of a boolean). ## Next steps Full API reference and code examples for OnDisconnect and presence in the Lark SDK. # Reading and writing data Source: https://docs.larksh.com/platform/read-write Core operations for getting data in and out of Lark Lark gives you a small set of operations that cover everything you need: writing data, reading it back, and a few special tools for common patterns. Every operation targets a specific path in your [JSON tree](/platform/data-structure). ## Set `set` overwrites the value at a path completely. Whatever was there before is gone, replaced by the new value. ```typescript theme={null} // Write an object await db.ref('players/alice').set({ name: 'Alice', score: 0 }); // Write a primitive value await db.ref('players/alice/score').set(42); ``` If you `set` an object, it replaces the entire object at that path. If Alice had an `online` field before, it's gone now, because the new object doesn't include it. `set` is a full replacement, not a merge. If you only want to update a few fields, use `update` instead. ## Update `update` performs a shallow merge at a path. It updates the keys you specify and leaves everything else untouched. ```typescript theme={null} // Only updates 'score' — 'name' and other fields are preserved await db.ref('players/alice').update({ score: 42 }); ``` This is what you want most of the time when modifying existing data. Changed the player's score? Update just the score. Toggled their online status? Update just that field. ### Multi-path atomic updates `update` also supports atomic writes across multiple paths. Prefix keys with `/` to write to absolute paths in a single atomic operation: ```typescript theme={null} await db.ref().update({ '/players/alice/score': 10, '/players/bob/score': 20, '/leaderboard/alice': 10, '/leaderboard/bob': 20 }); ``` All four writes happen together. Either they all succeed or none of them do. This is essential for keeping denormalized data consistent. When you store a score in two places, you want both to update at the same time. Multi-path updates are one of the most powerful tools in Lark. Any time you need to write to multiple locations atomically (updating a leaderboard, moving an item between lists, recording an action and its side effects), reach for a multi-path update. ## Remove `remove` deletes the data at a path. It's equivalent to calling `set` with `null`. ```typescript theme={null} // These two are equivalent await db.ref('players/alice').remove(); await db.ref('players/alice').set(null); ``` When you remove a node, its parent will also be removed if it has no other children. Lark doesn't store empty objects; the tree is pruned automatically. ## Push `push` generates a unique, chronologically sortable key and writes your data under it. This is perfect for lists where items are added over time: chat messages, event logs, game actions. ```typescript theme={null} const messagesRef = db.ref('messages'); const newRef = await messagesRef.push({ user: 'alice', text: 'Hello, world!', timestamp: ServerValue.TIMESTAMP }); console.log(newRef.key); // Something like '-OPNxyz123abc' ``` You can also call `push()` without any data to generate a key without writing anything yet. This is useful when you need the key upfront, for example to use it in a multi-path update: ```typescript theme={null} const newRef = messagesRef.push(); console.log(newRef.key); // Generated key, nothing written yet // Use the key in a set() or multi-path update later await newRef.set({ user: 'alice', text: 'Hello, world!', timestamp: ServerValue.TIMESTAMP }); ``` Push keys are designed to sort chronologically. If two clients push at the same time, both writes succeed and the keys maintain a consistent order. Push keys contain a timestamp component plus randomness. They sort in chronological order, so querying with `orderByKey` gives you messages in the order they were created. ## Once (read) `once` reads the current value at a path and returns a snapshot. It's a one-time read: unlike a subscription, it doesn't listen for future changes. ```typescript theme={null} const snapshot = await db.ref('players/alice').once('value'); if (snapshot.exists()) { console.log(snapshot.val()); // { name: 'Alice', score: 42 } } else { console.log('No data at this path'); } ``` The snapshot gives you the data as it exists on the server at that moment. Use `val()` to get the raw JSON value, `exists()` to check if there's data there, and `child()` to navigate into nested values. ## Server values Sometimes you want the server to fill in a value rather than the client. `ServerValue.TIMESTAMP` is replaced by the server's current time (in milliseconds since epoch) when the write is processed. ```typescript theme={null} await db.ref('players/alice').update({ lastSeen: ServerValue.TIMESTAMP }); ``` This ensures consistency. If you used `Date.now()` on the client, every device's clock would produce a slightly different value. With `ServerValue.TIMESTAMP`, every client agrees on the same time. Server values are resolved on write. When you read the data back, you'll see the actual timestamp number, not a placeholder. ## Optimistic writes When you write data, Lark applies the change to your local state immediately, before the server confirms it. Your UI updates instantly. Here's the flow: 1. You call `set`, `update`, or `remove`. 2. Lark applies the change locally right away. Any active subscriptions fire with the new data. 3. The write is sent to the server. 4. The server processes it (checking security rules, validating data). 5. If the server accepts, nothing else happens. Your local state was already correct. 6. If the server rejects (permissions, validation), Lark rolls back the local change and your subscriptions fire again with the corrected data. This means your app feels instant. Users see their changes reflected immediately, and in the vast majority of cases, the server will accept the write. On the rare occasion a write is rejected, the rollback happens seamlessly. Optimistic writes work with subscriptions. If you're subscribed to a path and you write to it, your subscription callback fires immediately with the new value, before the round trip to the server. ## What's next Order results, filter with ranges, and paginate through data. Listen for real-time changes with event types and delta sync. Atomic read-modify-write operations for safe concurrent updates. Validate incoming writes and control access. # Rules examples Source: https://docs.larksh.com/platform/rules-examples Common security rules patterns for real-world apps These are copy-paste patterns for the most common security rules scenarios. Each example includes the full rules JSON and a short explanation. For the complete reference of available variables and methods, see the [rules reference](/platform/rules-reference). ## Public read, authenticated write Anyone can read. Only logged-in users can write. ```json theme={null} { "rules": { "announcements": { ".read": true, ".write": "auth !== null" } } } ``` This is the simplest useful pattern. The `.read` rule is a plain `true`, so unauthenticated clients can read. The `.write` rule checks that `auth` is not null, meaning the client must be signed in to write. Good for public content like leaderboards, announcements, or game state that anyone can view. ## User-owned data Users can only read and write their own data. ```json theme={null} { "rules": { "users": { "$uid": { ".read": "auth.uid === $uid", ".write": "auth.uid === $uid" } } } } ``` The `$uid` wildcard captures the child key under `/users`. The rule compares it to `auth.uid`, so a user can only access the node that matches their own ID. No user can read or modify another user's data. ## Required fields validation Ensure that every write includes specific fields. ```json theme={null} { "rules": { "players": { "$playerId": { ".write": "auth.uid === $playerId", ".validate": "newData.hasChildren(['name', 'score'])", "name": { ".validate": "newData.isString() && newData.val().length >= 1" }, "score": { ".validate": "newData.isNumber()" }, "$other": { ".validate": false } } } } } ``` The top-level `.validate` ensures both `name` and `score` are present. Each child has its own validation to enforce types and constraints. The `$other` rule with `.validate: false` rejects any fields that aren't explicitly defined. This locks down your data shape tightly. The `$other` wildcard trick is a great way to prevent clients from writing unexpected fields. Any child key that doesn't match a named sibling rule falls through to `$other` and gets rejected. ## Type validation Check that values are the correct type. ```json theme={null} { "rules": { "profiles": { "$uid": { ".write": "auth.uid === $uid", "displayName": { ".validate": "newData.isString() && newData.val().length >= 3 && newData.val().length <= 30" }, "level": { ".validate": "newData.isNumber() && newData.val() >= 1 && newData.val() <= 100" }, "isPremium": { ".validate": "newData.isBoolean()" } } } } } ``` Use `isString()`, `isNumber()`, and `isBoolean()` to enforce types. Chain them with range checks or length constraints using `&&`. This prevents clients from sending a string where you expect a number, or a negative value where you expect a positive one. ## Role-based access Use an admin flag stored in user data to gate sensitive operations. ```json theme={null} { "rules": { "adminContent": { ".read": "root.child('users/' + auth.uid + '/role').val() === 'admin'", ".write": "root.child('users/' + auth.uid + '/role').val() === 'admin'" }, "users": { "$uid": { ".read": "auth.uid === $uid", ".write": "auth.uid === $uid", "role": { ".write": "root.child('users/' + auth.uid + '/role').val() === 'admin'", ".validate": "newData.isString() && (newData.val() === 'admin' || newData.val() === 'member')" } } } } } ``` The `/adminContent` path checks the requesting user's `role` field by reading from `/users/{uid}/role` via `root.child(...)`. Only users whose role is `'admin'` can read or write admin content. The `role` field itself can only be changed by an existing admin, preventing users from promoting themselves. Make sure the first admin account's role is set via the Lark dashboard or a server-side script. Otherwise, no one will have permission to set the initial admin role through the client SDK. ## Game lobby pattern Players can join a lobby by writing their own entry. Only the host can modify game settings. Anyone in the lobby can read all lobby data. ```json theme={null} { "rules": { "lobbies": { "$lobbyId": { ".read": "data.child('players/' + auth.uid).exists()", "settings": { ".write": "auth.uid === data.parent().child('host').val()", ".validate": "newData.hasChildren(['maxPlayers', 'gameMode'])" }, "host": { ".write": false }, "players": { "$uid": { ".write": "auth.uid === $uid && (!data.exists() || auth.uid === $uid)", ".validate": "newData.hasChildren(['name', 'ready'])", "name": { ".validate": "newData.isString()" }, "ready": { ".validate": "newData.isBoolean()" } } } } } } } ``` Here's what each piece does: * Only players already in the lobby can read its data. The `.read` rule checks if the authenticated user has an entry under `players/`. * Only the host (whose UID matches the `host` field) can update game settings like `maxPlayers` and `gameMode`. * The `host` field is locked down with `.write: false` at the client level. Set the host when creating the lobby via a server-side operation or initial write. * Each player can write only their own entry (`auth.uid === $uid`). Validation ensures every player entry has a `name` and `ready` status. The host field should be set when the lobby is first created. You can do this by including `host` in the initial write that creates the lobby, before per-field rules take effect. Alternatively, set it from a trusted server environment. ## Write-once data Data can be created but never modified or deleted. ```json theme={null} { "rules": { "events": { "$eventId": { ".read": true, ".write": "auth !== null && !data.exists()", ".validate": "newData.exists() && newData.hasChildren(['type', 'timestamp'])", "timestamp": { ".validate": "newData.isNumber() && newData.val() === now" } } } } } ``` The `.write` rule has two conditions: the user must be authenticated (`auth !== null`), and the data must not already exist (`!data.exists()`). Once a node is written, `data.exists()` becomes `true` and all future writes are rejected. The `.validate` rule also requires `newData.exists()`, which prevents deletion. Without this, a client could delete the node (setting it to `null`) and then re-create it. The `timestamp` validation ensures the timestamp is set to the server's current time via `now`, preventing clients from backdating events. Write-once data is perfect for audit logs, game events, chat messages, or any data that should be immutable after creation. ## Query-based access control Restrict reads so clients must include specific query parameters. This prevents clients from downloading an entire collection when they should only access a subset. ```json theme={null} { "rules": { "orders": { ".read": "auth !== null && query.orderByChild === 'userId' && query.equalTo === auth.uid" } } } ``` Clients must query by their own user ID to read orders. A bare `db.ref('orders').on('value', ...)` without the filter is rejected. This is useful when you have a shared collection but each user should only see their own records. You can also enforce size limits to prevent clients from downloading too much data at once: ```json theme={null} { "rules": { "feed": { ".read": "auth !== null && query.orderByKey && query.limitToFirst <= 100" } } } ``` ## Database-specific rules Use `lark.databaseId` and `lark.projectId` to write rules that are aware of which database they're running against. This is especially useful when you have one database per game session or room, since a single set of rules can enforce that users only access the database they've been assigned to. ```json theme={null} { "rules": { ".read": "auth.token.game_id === lark.databaseId", ".write": "auth.token.game_id === lark.databaseId" } } ``` Here, each user's auth token includes a `game_id` custom claim set by your server when the player joins a game. The rule compares it against `lark.databaseId`, the name of the database being accessed. A player assigned to game `match-4821` can read and write the `match-4821` database, but gets `PERMISSION_DENIED` if they try to connect to `match-9903`. One rule set covers every game database in the project. ## What's next Full reference for variables, snapshot methods, string methods, and operators. Understand how rules are structured, how cascading works, and where to edit them. # Rules reference Source: https://docs.larksh.com/platform/rules-reference Complete reference for security rules expressions This is the full reference for everything you can use inside Lark security rule expressions. If you're new to rules, start with the [security rules overview](/platform/security-rules) first. ## Variables These variables are available in every rule expression. | Variable | Type | Description | | ----------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `auth` | object \| null | The authenticated user. `null` if the client is unauthenticated. | | `auth.uid` | string | The unique ID of the authenticated user. | | `auth.provider` | string | The authentication provider (e.g., `"google"`, `"anonymous"`, `"password"`, `"facebook"`, `"github"`, `"twitter"`). | | `auth.token` | object | The contents of the auth token, including any custom claims. | | `data` | snapshot | A snapshot of the **current** data at this path, before the write. | | `newData` | snapshot | A snapshot of the data that **would exist** after the write. The merged result of new data and existing data. Only available in `.write` and `.validate` rules. | | `root` | snapshot | A snapshot of the root of the entire database. Use this to read data at other paths. | | `now` | number | The current server timestamp in milliseconds since epoch. | | `$wildcards` | string | Path segment captures. For example, a rule at `/users/$userId` makes `$userId` available as a string variable containing the matched key. | | `lark.projectId` | string | The ID of the current project. | | `lark.databaseId` | string | The ID of the current database (the database name, not including the project prefix). | `data` and `newData` are scoped to the current path where the rule is defined. Use `root` when you need to look up data elsewhere in your database. ## Snapshot methods The `data`, `newData`, and `root` variables are all snapshots. You can call these methods on any snapshot. | Method | Returns | Description | | ------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `val()` | any | Returns the primitive value (string, number, boolean, null) at this snapshot. For snapshots with children, use `child()` to access nested values. | | `exists()` | boolean | Returns `true` if this snapshot contains any data. Equivalent to `val() != null`. | | `hasChild(path)` | boolean | Returns `true` if the specified child path exists. `path` can be a single key or a slash-separated path. | | `hasChildren(keys)` | boolean | Returns `true` if **all** of the specified child keys exist. `keys` is an array of strings. With no arguments, returns `true` if the snapshot has any children. | | `parent()` | snapshot | Returns a snapshot of the parent node. Fails if called on the root. | | `child(path)` | snapshot | Returns a snapshot of the child at the given path. `path` can be a single key or a deeper slash-separated path (e.g., `'address/city'`). If the child doesn't exist, returns an empty snapshot. | | `getPriority()` | string \| number \| null | Returns the priority of the data at this snapshot. | | `isString()` | boolean | Returns `true` if the value is a string. | | `isNumber()` | boolean | Returns `true` if the value is a number. | | `isBoolean()` | boolean | Returns `true` if the value is a boolean. | ### Examples ``` // Check if the current data exists data.exists() // Check if the incoming data has both 'name' and 'score' children newData.hasChildren(['name', 'score']) // Read a sibling value data.parent().child('status').val() === 'active' // Look up data at another path entirely root.child('admins/' + auth.uid).exists() // Check a deeply nested child data.child('stats/hp').val() > 0 // Type-check incoming data newData.child('name').isString() && newData.child('score').isNumber() ``` ## String methods These methods are available on any string value in a rule expression, including `auth.uid`, `$wildcard` captures, and string values returned by `val()`. | Method / Property | Returns | Description | | --------------------------------- | ------- | --------------------------------------------------------------------------------------------- | | `length` | number | The length of the string. This is a property, not a method (no parentheses). | | `contains(substring)` | boolean | Returns `true` if the string contains the given substring. | | `beginsWith(substring)` | boolean | Returns `true` if the string starts with the given prefix. | | `endsWith(substring)` | boolean | Returns `true` if the string ends with the given suffix. | | `replace(substring, replacement)` | string | Returns a copy of the string with **all** instances of `substring` replaced by `replacement`. | | `toLowerCase()` | string | Returns the string converted to lower case. | | `toUpperCase()` | string | Returns the string converted to upper case. | | `matches(regex)` | boolean | Returns `true` if the string matches the given regular expression. | ### Examples ``` // Validate that a username is at least 3 characters newData.child('username').val().length >= 3 // Check that an email looks valid newData.child('email').val().matches(/^[^@]+@[^@]+\.[^@]+$/) // Check a string prefix auth.token.identifier.beginsWith('internal-') // Escape periods for use as keys newData.child('email').val().replace('.', '%2E') // Case-insensitive lookup root.child('users').child(auth.token.identifier.toLowerCase()).exists() ``` ## Operators Standard operators work inside rule expressions. ### Comparison | Operator | Description | | -------- | -------------------------------- | | `===` | Strict equality (type and value) | | `!==` | Strict inequality | | `<` | Less than | | `>` | Greater than | | `<=` | Less than or equal to | | `>=` | Greater than or equal to | `==` is treated as `===` and `!=` is treated as `!==` in rules. Both are strict comparisons; there is no loose equality. ### Logical | Operator | Description | | -------- | ----------- | | `&&` | Logical AND | | `\|\|` | Logical OR | | `!` | Logical NOT | ### Arithmetic | Operator | Description | | -------- | ------------------------------------ | | `+` | Addition (also string concatenation) | | `-` | Subtraction / negation | | `*` | Multiplication | | `/` | Division | | `%` | Modulo | ### Ternary | Operator | Description | | -------- | ---------------------------------------------------------------- | | `? :` | Conditional expression. `condition ? valueIfTrue : valueIfFalse` | Use the `+` operator to build dynamic paths for `child()` lookups: `root.child('users/' + auth.uid + '/role').val() === 'admin'`. ## Query-based rules You can restrict what queries clients are allowed to run by referencing `query.` expressions in your rules. This lets you enforce that clients include certain filters or limits. ### Query variables | Expression | Type | Description | | ----------------------- | ----------------------------------- | -------------------------------------------------------------------- | | `query.orderByKey` | boolean | `true` if the query is ordered by key. | | `query.orderByPriority` | boolean | `true` if the query is ordered by priority. | | `query.orderByValue` | boolean | `true` if the query is ordered by value. | | `query.orderByChild` | string \| null | The child key being ordered by, or `null` if not ordered by a child. | | `query.startAt` | string \| number \| boolean \| null | The start bound of the query, or `null` if none. | | `query.endAt` | string \| number \| boolean \| null | The end bound of the query, or `null` if none. | | `query.equalTo` | string \| number \| boolean \| null | The equality filter of the query, or `null` if none. | | `query.limitToFirst` | number \| null | The limit-to-first value, or `null` if none. | | `query.limitToLast` | number \| null | The limit-to-last value, or `null` if none. | ### Examples Restrict access to a collection so clients can only read their own items: ```json theme={null} { "rules": { "baskets": { ".read": "auth.uid !== null && query.orderByChild === 'owner' && query.equalTo === auth.uid" } } } ``` A query that includes the required parameters succeeds: ```typescript theme={null} db.ref('baskets') .orderByChild('owner') .equalTo(auth.currentUser.uid) .on('value', callback); // Succeeds ``` A query without the parameters fails: ```typescript theme={null} db.ref('baskets') .on('value', callback); // PERMISSION_DENIED ``` Limit how much data a client can download: ```json theme={null} { "rules": { "messages": { ".read": "query.orderByKey && query.limitToFirst <= 1000" } } } ``` ## What's next See these building blocks in action with real-world patterns. Go back to the overview to understand how rules are structured and evaluated. # Security rules Source: https://docs.larksh.com/platform/security-rules Control who can read and write your data Security rules are the gatekeepers of your Lark database. They're declarative JSON expressions that live alongside your data structure, and they're evaluated entirely on the server. No matter what a client sends, the rules decide whether the operation goes through. ## How rules work You define rules as a JSON object that mirrors the shape of your database. At each path, you can attach three rule types: * `.read`: Can this client read data at this path? * `.write`: Can this client write data at this path? * `.validate`: Is the incoming data valid? Access is **denied by default**. If no `.read` or `.write` rule is specified at or above a path, the operation is rejected. Rules evaluate to `true` or `false`. You can use simple booleans or expressions that reference auth state, existing data, and the incoming write. ```json theme={null} { "rules": { "players": { "$playerId": { ".read": true, ".write": "auth.uid === $playerId" } } } } ``` In this example, anyone can read any player's data, but only the player themselves can write to their own node. ## Cascading rules `.read` and `.write` rules **cascade downward**. If you grant `.read` at `/users`, every child path under `/users` is also readable. A rule at a parent node overrides any restrictive rule on a child. Once access is granted, it can't be taken away deeper in the tree. ```json theme={null} { "rules": { "foo": { ".read": "data.child('baz').val() === true", "bar": { ".read": false } } } } ``` Here, the `.read: false` on `/foo/bar` has no effect. If `/foo/baz` is `true`, the parent rule grants read access to all of `/foo`, including `/foo/bar`. Child rules can only grant additional privileges, never revoke them. Be intentional about where you place `.read` and `.write` rules. A permissive rule high in the tree grants access to everything below it. `.validate` rules are the exception. They do **not** cascade. Every node that has a `.validate` rule must independently pass validation for a write to succeed. This lets you enforce structure at every level of your data. ## Rules are not filters Rules are evaluated atomically. A read operation fails entirely if there isn't a rule at that location (or a parent) that grants access, even if every individual child is accessible. ```json theme={null} { "rules": { "records": { "rec1": { ".read": true }, "rec2": { ".read": false } } } } ``` Reading `/records` fails with `PERMISSION_DENIED`, even though `rec1` is readable. There's no `.read` rule that covers all of `/records`, so the read is denied. To get `rec1`, you must read `/records/rec1` directly. This is an important design point: rules don't filter results down to what's allowed. They're all-or-nothing at each path. ## Wildcards Use `$wildcard` path segments to match any child key. The matched value becomes available as a variable in your rule expressions. ```json theme={null} { "rules": { "rooms": { "$roomId": { ".read": true, "messages": { "$messageId": { ".write": "auth !== null", ".validate": "newData.hasChildren(['text', 'author'])" } } } } } } ``` Here, `$roomId` matches any room key and `$messageId` matches any message key. You can reference these variables in expressions, for example `"auth.uid === $roomId"` to restrict access to the room owner. Wildcard captures are always strings, even if the key looks like a number. Comparing `$key` to a number directly will always fail. Convert the number to a string first: `$key === newData.val() + ''`. ### Restricting allowed children You can use a `$other` wildcard alongside named children to reject any unexpected fields: ```json theme={null} { "rules": { "widget": { "title": { ".validate": true }, "color": { ".validate": true }, "$other": { ".validate": false } } } } ``` Any write that includes a child other than `title` or `color` fails validation. ### Overlapping statements When both a `$wildcard` rule and a specific named rule match a node, both are evaluated. If either condition is `false`, access is denied. ```json theme={null} { "rules": { "messages": { "$message": { ".read": true, ".write": true }, "message1": { ".read": false, ".write": false } } } } ``` Reads to `message1` are denied because the specific `message1` rule evaluates to `false`, even though the `$message` wildcard rule is `true`. ## Volatile path restrictions [Volatile paths](/platform/volatile) support `.read`, `.write`, and `.validate` rules, but those rules cannot reference `data` or `root`. Reading existing database state is incompatible with the volatile fast path. If a rule on a volatile path contains `data.*` or `root.*`, it evaluates to `false`. See [volatile paths](/platform/volatile#rules-restrictions-on-volatile-paths) for the full list of what's allowed. ## Editing rules You edit your security rules in the Lark dashboard. Go to **Project Settings > Security Rules**, paste or edit your JSON, and click **Publish**. Rules take effect immediately across all connected clients. Start with restrictive rules and open access only where you need it. ## What's next Full reference for every variable, method, and operator available in rule expressions. Copy-paste patterns for common scenarios: user-owned data, role-based access, write-once data, and more. # Sorting and filtering Source: https://docs.larksh.com/platform/sorting How Lark orders data and how to query subsets of it When you subscribe to or read a list of children, Lark returns them in a defined order. You control that order with query methods (sorting by key, by value, or by a nested child field) and you can filter the results down to just the slice you need. ## Default ordering By default, children are sorted by their key. This is the order you get when you read a path without specifying any ordering. ## Ordering modes Lark supports four ordering modes. You pick one per query. ### Order by key Sorts children by their key name. Keys that are valid 32-bit integers are sorted numerically first, then remaining string keys are sorted lexicographically. ```typescript theme={null} // Get all players sorted by their key db.ref('players').orderByKey() ``` This is the default behavior, but specifying it explicitly makes your intent clear. ### Order by value Sorts children by their value directly. This works when the children are primitive values (strings, numbers, booleans), not objects. ```typescript theme={null} // Scores stored as: { "alice": 250, "bob": 180, "charlie": 300 } db.ref('scores').orderByValue() ``` ### Order by child Sorts children by the value of a nested child field. This is the one you'll use most often. ```typescript theme={null} // Sort players by their 'score' field db.ref('players').orderByChild('score') ``` You can also sort by deeply nested fields using a path: ```typescript theme={null} // Sort players by a nested field db.ref('players').orderByChild('stats/hp') db.ref('users').orderByChild('address/city') ``` If a child doesn't have the specified field, it sorts as if the value is `null` (which comes first in Lark's sort order). ### Order by priority Sorts children by their priority metadata. Priority is a legacy ordering mechanism, and in most cases `orderByChild` is a better choice. But it's available when you need it. ```typescript theme={null} db.ref('players').orderByPriority() ``` ## Value type precedence When sorting, Lark follows a strict type ordering. If your data contains mixed types, they sort in this order: | Order | Type | Sorting within type | | ----- | ------- | ----------------------------- | | 1 | `null` | All nulls are equal | | 2 | `false` | — | | 3 | `true` | — | | 4 | Numbers | Ascending numeric order | | 5 | Strings | Lexicographic (Unicode) order | | 6 | Objects | Sorted by key | Numbers always come before strings. So the value `25` sorts before the value `"apple"`, regardless of what they'd look like alphabetically. This type ordering matters most when using `orderByValue` or `orderByChild` on data where different children might have different types for the same field. In practice, keep your types consistent and you won't need to think about this. ## Limits Limits let you cap how many results are returned. ### limitToFirst Returns the first N results in the current sort order. ```typescript theme={null} // Get the 5 players with the lowest scores db.ref('players').orderByChild('score').limitToFirst(5) ``` ### limitToLast Returns the last N results in the current sort order. ```typescript theme={null} // Get the 5 players with the highest scores db.ref('players').orderByChild('score').limitToLast(5) ``` `limitToLast` is your friend for "top N" queries. Since `orderByChild('score')` sorts ascending, `limitToLast(10)` gives you the top 10 highest scores. ## Ranges Ranges let you filter results to a specific window within the sort order. ### startAt / endAt (inclusive) `startAt` and `endAt` define inclusive bounds. Only results that fall within the range are returned. ```typescript theme={null} // Players with scores between 100 and 500 (inclusive) db.ref('players').orderByChild('score').startAt(100).endAt(500) ``` ### startAfter / endBefore (exclusive) `startAfter` and `endBefore` are the exclusive versions. They exclude the boundary value itself. ```typescript theme={null} // Players with scores strictly greater than 100 db.ref('players').orderByChild('score').startAfter(100) ``` ### Key tiebreaker When multiple children have the same sort value, `startAt`, `endAt`, `startAfter`, `endBefore`, and `equalTo` all accept an optional second parameter (a key) to disambiguate. This is essential for pagination over data with duplicate values. ```typescript theme={null} // Two players both have score 250. To start *after* the one with key "alice": db.ref('players').orderByChild('score').startAt(250, 'alice').limitToFirst(10) ``` Without the key parameter, `startAt(250)` would include all children with score 250. With the key, Lark skips past `alice` and starts at the next child with that score (or the next score above it). ### equalTo `equalTo` matches exactly one value. It's a shorthand for setting `startAt` and `endAt` to the same value. ```typescript theme={null} // Find all players with a score of exactly 250 db.ref('players').orderByChild('score').equalTo(250) ``` Combined with `orderByKey`, `equalTo` works as a direct key lookup: ```typescript theme={null} // Look up a specific child by key db.ref('players').orderByKey().equalTo('alice') ``` ## Combining ordering, limits, and ranges You can chain one ordering mode with any combination of limits and ranges to build precise queries. Here's a practical example, a leaderboard showing the top 10 scores: ```typescript theme={null} // Subscribe to the top 10 players by score db.ref('players') .orderByChild('score') .limitToLast(10) .on('value', (snapshot) => { const leaderboard: Array<{ name: string; score: number }> = []; snapshot.forEach((childSnapshot) => { leaderboard.push(childSnapshot.val()); }); // Results are in ascending order, reverse for top-down display leaderboard.reverse(); console.log('Leaderboard:', leaderboard); }); ``` Or paginating through chat messages: ```typescript theme={null} // Get the 20 most recent messages db.ref('messages') .orderByKey() .limitToLast(20) .once('value'); // Get the next 20 messages before a known key db.ref('messages') .orderByKey() .endBefore(oldestMessageKey) .limitToLast(20) .once('value'); ``` You can only use one `orderBy` method per query. You cannot, for example, sort by `score` and then sub-sort by `name`. If you need compound sorting, structure your data so a single field captures the sort order you need (e.g., a composite key like `"0250_alice"`). # Subscriptions Source: https://docs.larksh.com/platform/subscriptions Listen for real-time data changes across all connected clients Subscriptions are how you get live updates from Lark. You subscribe to a path, and your callback fires whenever the data at that path changes, no matter which client made the change. ## Event types Lark supports five event types, each suited for different use cases. | Event | When it fires | | --------------- | -------------------------------------------------------------------------------------------------------------------------- | | `value` | Fires with the entire snapshot whenever anything under the path changes. The simplest and most common event type. | | `child_added` | Fires once for each existing child, then again whenever a new child is added. Includes a `prevChildKey` for sort ordering. | | `child_changed` | Fires when an existing child's value changes. | | `child_removed` | Fires when a child is removed from the path. | | `child_moved` | Fires when a child's sort order changes (relevant with `orderBy` queries). | `value` is all you need for most use cases. The child events are for working with large collections where you want granular control: adding, updating, and removing individual items without re-processing the entire list. ## How delta sync works Lark doesn't re-send the entire dataset every time something changes. When you subscribe to a path, the server computes the minimal delta and sends only what changed. The client-side SDK assembles these deltas into full snapshots before calling your callbacks. This means subscribing to a path with 10,000 children doesn't re-transmit all 10,000 children when one of them changes. The server sends a small delta describing the change, and your local SDK patches its cached copy. You don't need to think about this. Your callbacks always receive complete snapshots. But it's useful to know that Lark is efficient under the hood, especially for large datasets. ## Shared views Multiple subscriptions to the same path and query share a single server-side view. If 100 clients subscribe to the same leaderboard with the same query, the server does the same work as it would for one subscription. The data is computed once and broadcast to all subscribers. This is why Lark scales well for real-time features like leaderboards, live dashboards, and game state. The work is per unique query, not per subscriber. Shared views apply when the path and query are identical. Two subscriptions to the same path but with different `orderBy` or `limitTo` parameters create separate server-side views. ## One-time reads vs. subscriptions If you only need the current value and don't care about future changes, use a one-time read instead of a subscription. It fetches the data once and doesn't keep a live connection to that path. One-time reads are useful for loading initial state, checking a value before writing, or fetching data that rarely changes. ## Next steps Full API and code examples for real-time subscriptions in the Lark SDK. Subscribe to real-time updates over HTTP with Server-Sent Events. # Transactions Source: https://docs.larksh.com/platform/transactions Atomic read-modify-write operations and multi-path updates When multiple clients write to the same data at the same time, you can get race conditions. Transactions give you atomic read-modify-write operations so that concurrent updates don't step on each other. ## Transaction use cases Imagine two players both try to increment a counter at the same time. Without transactions: 1. Client A reads the counter: `10` 2. Client B reads the counter: `10` 3. Client A writes `11` 4. Client B writes `11` The counter should be `12`, but it's `11`. One increment was lost. Transactions prevent this. ## Callback-style transactions The most common pattern. You provide a function that receives the current value and returns the new value. Lark handles the retry logic. Here's what happens under the hood: 1. The SDK reads the current value at the path. 2. Your function runs, receiving the current value and returning the new value. 3. The SDK sends the new value to the server along with the value of the path that your client saw when it made the change. 4. If another client changed the value between your read and write, the server rejects the transaction. 5. Your function is called again with the updated value. 6. This repeats until the write succeeds or the retry limit is reached (25 attempts). Your transaction function may be called multiple times. It should be a **pure function**: no side effects, no network requests, no UI updates. Just compute a new value from the current value. Return `undefined` from your function to abort the transaction without writing anything. ## Optimistic behavior Transactions are optimistic by default. The client applies the result of your function to local state immediately, so your UI updates right away. If the server rejects the transaction and your function retries, the local state is updated again with the new result. This means your subscriptions fire immediately with the optimistic value, then potentially fire again if the server computes a different outcome. In practice, retries are rare and the optimistic value is almost always correct. ## Multi-path transactions Sometimes you need to update multiple paths atomically, where either all the changes happen or none of them do. Multi-path transactions support two syntaxes: * **Object syntax**: Pass an object where keys are paths and values are what to write. Simple and concise for straightforward atomic writes. * **Array syntax**: Pass an array of operations for more control, including the ability to add conditions. ## Conditions (compare-and-swap) In array-syntax transactions, you can add `condition` operations that check the current value before proceeding. If any condition fails, the entire transaction is rejected and no writes are applied. For primitive values (strings, numbers, booleans), the condition compares the value directly. For complex objects, to be efficient and avoid sending the entire object's data over the wire, Lark computes a SHA-256 hash of the current value and compares that. This lets you condition on "this object hasn't changed" without specifying every field. This is handled for you and you don't need to worry about how calculating the hash yourself, but it's useful to know how to works in the background. Use callback-style transactions for simple read-modify-write operations on a single path (like counters). Use multi-path transactions with conditions when you need to update several paths atomically or enforce preconditions. ## Next steps Full API reference and code examples for transactions in the Lark SDK. Compare-and-swap over HTTP using ETags. # Transports Source: https://docs.larksh.com/platform/transports WebSocket and WebTransport connections to Lark Lark supports two transport protocols for client-server communication. The SDK defaults to WebSocket, but you can opt in to WebTransport for lower latency in supported browsers. ## WebSocket WebSocket is the default transport. It works everywhere: all browsers, Node.js, React Native, and any environment that supports standard WebSocket connections. | | | | ------------- | ------------------------------------------------------------------------------ | | URL | `wss://{projectId}.larkdb.net/ws` | | Delivery | Reliable, ordered TCP delivery. Every message arrives, in order. | | Compatibility | Universal. If your client can make a network connection, it can use WebSocket. | WebSocket is a mature, battle-tested protocol. For most applications, it's the only transport you need. ## WebTransport WebTransport is a modern protocol built on HTTP/3 and QUIC. It's available in newer browsers and provides meaningful advantages for real-time applications. | | | | ------------- | ----------------------------------------------------------------------------------------------------- | | URL | `https://{projectId}.larkdb.net:{port}/wt` (port randomly assigned from 7778-7809 for load balancing) | | Delivery | QUIC streams for reliable data, plus UDP datagrams for [volatile paths](/platform/volatile). | | Compatibility | Chrome 97+, Edge 97+, Firefox 114+. Not yet available in Safari, Node.js, or React Native. | ### WebTransport advantages WebTransport has two key advantages over WebSocket: **No head-of-line blocking.** With WebSocket (TCP), if one packet is lost, every subsequent packet waits until the lost one is retransmitted. A dropped packet carrying a chat message stalls cursor updates, game state, everything. With WebTransport (QUIC), each stream is independent. A lost packet on one stream doesn't block other streams. **UDP datagrams for volatile data.** Volatile writes over WebTransport use UDP datagrams, providing unreliable, unordered delivery with zero retransmission overhead. For data like cursor positions where you only care about the latest value, this is ideal. A lost datagram is irrelevant because a newer one is already on the way. These advantages matter most under packet loss, which is common on mobile networks, congested Wi-Fi, and cross-continent connections. On a clean local network, WebSocket and WebTransport perform similarly. ## Transport selection By default, the SDK uses WebSocket. To use WebTransport, set the `transport` option explicitly: ```typescript theme={null} // Default — WebSocket only const db = new LarkDatabase('my-project/my-database', { anonymous: true, }); // Try WebTransport first, fall back to WebSocket const db = new LarkDatabase('my-project/my-database', { anonymous: true, transport: 'auto', }); // Force WebTransport only (will fail if not supported) const db = new LarkDatabase('my-project/my-database', { anonymous: true, transport: 'webtransport', }); // Auto with a custom timeout for the WebTransport attempt const db = new LarkDatabase('my-project/my-database', { anonymous: true, transport: 'auto', webTransportTimeout: 5000, // 5 seconds (default is 2 seconds) }); ``` In `'auto'` mode, the SDK tries WebTransport first. If the connection fails or doesn't establish within `webtransportTimeout` milliseconds, it falls back to WebSocket transparently. ## When to prefer WebTransport Choose WebTransport when: * Your app uses [volatile paths](/platform/volatile) for game state, cursors, or other high-frequency data. The UDP datagram support gives you faster, lower-overhead delivery. * Your users are on modern browsers (Chrome, Edge, or Firefox). * Your app is latency-sensitive and your users may be on spotty connections where head-of-line blocking hurts. ## When WebSocket is fine WebSocket is the right choice for most applications. It's mature, universal, and performant. You won't notice a difference unless you're pushing high-frequency volatile updates or operating under significant packet loss. Stick with the default WebSocket when: * Your app doesn't use volatile paths. * You need to support Safari, Node.js, or React Native. * Your users are on reliable connections. * You just want things to work everywhere without thinking about it. If you want the best of both worlds, set `transport: 'auto'`. Your Chrome and Edge users get the benefits of WebTransport, and everyone else gets reliable WebSocket. ## What's next High-frequency data that takes full advantage of WebTransport datagrams. Configure transports, timeouts, and connection lifecycle in the Lark SDK. # Volatile paths Source: https://docs.larksh.com/platform/volatile High-frequency, fire-and-forget data for game state and cursors Some data changes many times per second: cursor positions, player movement, animation state, drag coordinates. Sending every update through reliable, ordered delivery is overkill. Volatile paths give you a fast lane for this kind of data. ## How volatile paths work Volatile paths are special paths in your database optimized for high-frequency updates. When you write to a volatile path: 1. The write resolves immediately on the client. There's no waiting for a server acknowledgment. 2. The server batches and coalesces volatile updates before broadcasting them. If a client writes 60 cursor positions per second, subscribers don't receive all 60. They get the latest value at each broadcast interval. 3. Only the most recent value for each path is sent. Intermediate states are dropped. The result is low-latency, low-bandwidth data delivery, perfect for real-time interactions where you only care about the current state. ```typescript theme={null} // Writing to a volatile path — resolves instantly db.ref('cursors/alice').set({ x: 450, y: 312 }); ``` ## Volatile vs. regular writes | | Regular writes | Volatile writes | | ------------------ | ---------------------------------------------- | ----------------------------------------------- | | **Acknowledgment** | Server confirms the write | Resolves immediately, no server ack | | **Denials** | Server sends an error the client can handle | Silently dropped | | **Delivery** | Reliable, ordered | Best-effort, latest-value-wins | | **Persistence** | Written to durable storage | Not persisted, in-memory only | | **Broadcast** | Every change is sent to subscribers | Batched and coalesced before broadcast | | **Transport** | TCP (WebSocket) or QUIC streams (WebTransport) | TCP (WebSocket) or UDP datagrams (WebTransport) | Over WebTransport, volatile writes use UDP datagrams for truly unreliable delivery at maximum speed. Over WebSocket, they're sent as regular TCP messages but are still batched and coalesced on the server before broadcast. ## Batching rates The server broadcasts volatile updates at fixed intervals: * **WebTransport clients**: \~20 Hz (every 50ms). At most 20 volatile updates per second reach each subscriber. * **WebSocket clients**: \~7 Hz (every 150ms). Slower than WebTransport due to TCP overhead, but still fast enough for smooth updates. We recommend tuning the frequency at which you write to volatile paths to match the transport the client is connected on. For example, on WebSocket you might write 10 times per second, while on WebTransport you could write 20 times per second. ## Size limit Each volatile write is limited to **2 KB**. Keep your volatile payloads small: coordinates, velocity vectors, flags. If you need to send more, break it into multiple paths. The 2 KB limit is enforced per write. If your volatile payload exceeds it, the write will be dropped. Stick to small, focused data: positions, not full game state. ## Configuring volatile paths Mark a path as volatile by adding `".volatile": true` in your [security rules](/platform/security-rules): ```json theme={null} { "rules": { "cursors": { "$userId": { ".volatile": true, ".read": true, ".write": "auth.uid === $userId" } } } } ``` Any path under a `.volatile` rule automatically uses volatile delivery. You don't need to change your write code. The same `set()` and `update()` calls just behave differently under the hood. ### Rules restrictions on volatile paths Volatile paths support `.read`, `.write`, and `.validate` rules, but they must be **simple rules** that don't reference existing database state. Specifically, a rule on a volatile path cannot contain `data.` or `root.` anywhere in the expression. These require reading current state from storage, which is incompatible with the volatile fast path. Any rule variable that doesn't require reading existing state (`auth`, `newData`, `$wildcards`, `now`, literals, and operators) works fine. The two that don't work are `data` and `root`, since both require reading current database state. If a volatile path has a rule that references `data` or `root`, the rule evaluates to `false`, and the read or write is silently denied. There's no error message; the operation just fails. Make sure your volatile path rules only use the allowed variables listed above. ## Volatile snapshots When you receive a volatile update through a subscription, the snapshot includes extra metadata: ```typescript theme={null} db.ref('cursors/bob').on('value', (snapshot) => { if (snapshot.isVolatile()) { const serverTime = snapshot.getServerTimestamp(); const position = snapshot.val(); // Use server timestamp for interpolation interpolateCursor(position, serverTime); } }); ``` * **`snapshot.isVolatile()`**: Returns `true` if this update arrived via volatile delivery. * **`snapshot.getServerTimestamp()`**: The server timestamp when the volatile value was broadcast. Use this for interpolation or lag compensation in games. ## When to use volatile paths Volatile paths are the right choice when: * The data changes many times per second * You only care about the latest value, not every intermediate state * Missing an update is acceptable (the next one will arrive shortly) **Good candidates:** * Cursor and pointer positions * Player position and velocity in games * Typing indicators * Drag-and-drop coordinates * Animation state and transforms * Camera position in collaborative 3D editors **Bad candidates:** * Scores and leaderboards (every increment matters) * Inventory and currency (can't afford to miss a change) * Chat messages (users expect to see every message) * Game actions and events (order and completeness matter) Volatile data is not persisted to disk. It lives in memory only. If the server restarts, volatile data is gone. This is by design: volatile paths are for ephemeral, high-frequency data where the next update is always right behind the current one. ## What's next WebSocket vs WebTransport, and how volatile data benefits from UDP datagrams. Size limits and rate limits, including volatile exceptions. # Quickstart Source: https://docs.larksh.com/quickstart Create a project and start building in minutes ## 1. Create a project Head to the [Lark dashboard](https://dashboard.lark.sh) and sign in with Google. Click **New Project** and give it a name. This generates a project ID you'll use to connect. In your project's **Settings** page, note your **Project ID**. You'll need it in the next step. Enable **Auto Create** in your project settings so databases are created automatically when a client connects. This is the fastest way to get started. ## 2. Pick your SDK The native TypeScript SDK. Lightweight (\~20KB), WebTransport support, volatile paths, modern API. Already using Firebase? Change one URL and your existing code works with Lark. Supports JS, Android, iOS, Flutter, C++, and Unity. ## 3. A quick example with the Lark SDK If you're starting fresh, here's what it looks like end-to-end: ### Install ```bash theme={null} npm install @lark-sh/client ``` ### Connect and write data ```typescript theme={null} import { LarkDatabase } from '@lark-sh/client'; // Connect to your project and database const db = new LarkDatabase('your-project-id/my-first-database', { anonymous: true }); // Write some data await db.ref('players/alice').set({ name: 'Alice', score: 0 }); ``` The `anonymous: true` option connects without authentication, which is useful for getting started quickly. You'll add proper auth later. ### Read data ```typescript theme={null} const snapshot = await db.ref('players/alice').once('value'); console.log(snapshot.val()); // { name: 'Alice', score: 0 } ``` ### Subscribe to real-time updates Subscribe to a path and your callback fires every time the data changes, from any connected client. ```typescript theme={null} const unsubscribe = db.ref('players').on('value', (snapshot) => { console.log('Players:', snapshot.val()); }); // Update a score — every subscriber sees it instantly await db.ref('players/alice/score').set(42); // When you're done listening unsubscribe(); ``` ### Clean up ```typescript theme={null} await db.disconnect(); ``` ## Developer tools ### Lark CLI The [Lark CLI](/cli/overview) lets you manage projects, databases, data, and security rules from the terminal: ```bash theme={null} npm install -g @lark-sh/cli lark login lark config set-project your-project-id ``` Once set up, you can read and write data, export and import databases, deploy security rules, and monitor your project, all without leaving the terminal. See the [CLI overview](/cli/overview) for common workflows. ### Claude Code plugin If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code), install the Lark plugin so Claude has full context on the Lark platform: ```bash theme={null} /plugin marketplace add lark-sh/lark-skill /plugin install lark@lark-tools ``` Use `/lark` in any conversation to activate it, or Claude will load it automatically when you're working with Lark. See the [Claude Code plugin](/cli/claude-code) page for details. ## What's next? Understand how Lark organizes data as a JSON tree. Lock down who can read and write your data. Learn about real-time event types and how delta updates work. Explore your project's databases, metrics, and settings. # Conditional requests Source: https://docs.larksh.com/rest-api/conditional Use ETags for compare-and-swap writes Lark supports conditional writes using ETags. This gives you compare-and-swap (CAS) semantics over HTTP: write only if the data hasn't changed since you last read it. ## How it works 1. Read data and request its ETag. 2. When you're ready to write, include the ETag in your request. 3. If the data has changed since you read it (ETag mismatch), the write fails with `412 Precondition Failed`. 4. If the data is unchanged, the write succeeds. This prevents lost updates when multiple clients or servers are writing to the same path. ## Getting an ETag Add the `X-Firebase-ETag: true` header to any `GET` request: ```bash theme={null} curl -i \ -H 'X-Firebase-ETag: true' \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' ``` Response headers include the ETag: ``` HTTP/2 200 ETag: "a1b2c3d4e5f6..." Content-Type: application/json {"name":"Alice","score":250,"online":true} ``` The ETag is a SHA-256 hash of the data's canonical JSON representation. ## Conditional write Include the ETag in an `If-Match` header on your write: ```bash theme={null} curl -X PUT \ -H 'If-Match: "a1b2c3d4e5f6..."' \ 'https://my-game--chess-app.larkdb.net/players/alice/score.json?auth=YOUR_TOKEN' \ -d '300' ``` ### Success If the ETag matches (data hasn't changed), the write succeeds normally: ``` HTTP/2 200 300 ``` ### Conflict If someone else modified the data between your read and write, you get a `412`: ``` HTTP/2 412 {"error":"condition_failed","message":"ETag mismatch"} ``` The response body contains the current value and the response includes the updated ETag, so you can read the new state and retry. ## Retry pattern A typical conditional update loop: ```typescript theme={null} async function incrementScore(projectUrl: string, path: string, token: string) { const url = `${projectUrl}/${path}.json?auth=${token}`; while (true) { // 1. Read with ETag const readResponse = await fetch(url, { headers: { 'X-Firebase-ETag': 'true' } }); const etag = readResponse.headers.get('ETag'); const currentValue = await readResponse.json(); // 2. Compute new value const newValue = (currentValue ?? 0) + 1; // 3. Conditional write const writeResponse = await fetch(url, { method: 'PUT', headers: { 'If-Match': etag! }, body: JSON.stringify(newValue) }); if (writeResponse.ok) { return newValue; // Success } if (writeResponse.status === 412) { continue; // Conflict — retry with fresh data } throw new Error(`Unexpected status: ${writeResponse.status}`); } } ``` ## When to use conditional requests Conditional writes are useful for: * Incrementing counters and balances without risking double-counts. * Optimistic locking: let a user edit data and reject stale writes. * Server-side read-modify-write operations without holding a persistent connection. For client-side compare-and-swap, [transactions](/platform/transactions) in the Lark SDK provide a more ergonomic API with automatic retries. # REST API Source: https://docs.larksh.com/rest-api/overview Read and write data over HTTP, no SDK required Lark exposes a REST API for reading and writing data over plain HTTP. If you're working from a server, a script, a CLI tool, or any environment where you don't want to maintain a persistent connection, the REST API is the way to go. ## URL format Every REST request targets a path in a database, with `.json` appended. The database goes in the subdomain, separated from your project ID by `--`: ``` https://{database}--{projectId}.larkdb.net/{path}.json ``` For example, to access `/players/alice` in the `my-game` database of project `chess-app`: ``` https://my-game--chess-app.larkdb.net/players/alice.json ``` The root of the database: ``` https://my-game--chess-app.larkdb.net/.json ``` ### The default database If you leave the database out of the subdomain, Lark routes to a database named `default`: ``` https://chess-app.larkdb.net/players/alice.json ``` That request is identical to: ``` https://default--chess-app.larkdb.net/players/alice.json ``` ## Authentication Pass an auth token as a query parameter: ``` GET https://my-game--chess-app.larkdb.net/players.json?auth=YOUR_TOKEN ``` The `auth` parameter accepts any token that Lark supports: [Lark tokens](/platform/authentication) (HS256 JWTs signed with your secret key) or [Firebase Auth tokens](/firebase/auth). You can also use `access_token` as an alias: ``` GET https://my-game--chess-app.larkdb.net/players.json?access_token=YOUR_TOKEN ``` If you omit the token, the request is treated as anonymous. Your [security rules](/platform/security-rules) determine what anonymous clients can access. ## Operations at a glance | HTTP method | Operation | Description | | ----------- | --------- | -------------------------------------- | | `GET` | Read | Fetch data at a path | | `PUT` | Set | Replace data at a path | | `POST` | Push | Add a child with an auto-generated key | | `PATCH` | Update | Merge data at a path | | `DELETE` | Remove | Delete data at a path | ## When to use the REST API * Reading or writing data from your backend without maintaining a WebSocket connection. * One-off scripts to import data, seed a database, or run migrations. * Receiving a webhook and writing to Lark with a single HTTP call. * Edge functions, serverless workers, or other runtimes without WebSocket support. * Quick reads with `curl` to inspect your data. For real-time subscriptions and persistent connections, use the [Lark SDK](/lark-sdk/overview) or [Firebase SDKs](/firebase/overview) instead. ## Quick example ```bash theme={null} # Write data curl -X PUT \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' \ -d '{"name": "Alice", "score": 0}' # Read it back curl 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' # {"name":"Alice","score":0} # Update a single field curl -X PATCH \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' \ -d '{"score": 42}' # Push a new child with auto-generated key curl -X POST \ 'https://my-game--chess-app.larkdb.net/messages.json?auth=YOUR_TOKEN' \ -d '{"text": "Hello!", "sender": "alice"}' # {"name":"-Kabc123xyz"} # Delete data curl -X DELETE \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' ``` ## What's next GET requests, shallow reads, queries, and formatting options. PUT, POST, PATCH, DELETE, multi-path updates, and server values. Server-Sent Events for real-time updates over HTTP. ETags and compare-and-swap for safe concurrent writes. # Reading data Source: https://docs.larksh.com/rest-api/reading Fetch data from your database with GET requests Read data by sending a `GET` request to any path with `.json` appended. ## Basic read ```bash theme={null} curl 'https://my-game--chess-app.larkdb.net/players/alice.json' ``` Response: ```json theme={null} { "name": "Alice", "score": 250, "online": true } ``` If the path doesn't exist, the response is `null`. ## Shallow reads For large datasets, you often don't need the entire subtree. Add `shallow=true` to get only the immediate children without recursing into them: ```bash theme={null} curl 'https://my-game--chess-app.larkdb.net/players.json?shallow=true' ``` By default, shallow reads replace every child with `true`. You see which keys exist, but nothing about their values or size: ```json theme={null} { "alice": true, "bob": true, "carol": true } ``` ### Enhanced shallow reads Add `v=2` for a more useful shallow response. With `v=2`, primitive values (strings, numbers, booleans) are returned as-is, and container objects are returned as `{".sz": N}` where N is the size in bytes: ```bash theme={null} curl 'https://my-game--chess-app.larkdb.net/.json?shallow=true&v=2' ``` ```json theme={null} { "players": {".sz": 4096}, "settings": "dark", "playerCount": 42, "active": true } ``` This tells you a lot more at a glance. `players` is an object containing \~4 KB of data, while `settings`, `playerCount`, and `active` are leaf values you can read directly. Useful for exploring an unfamiliar database or deciding which subtrees are worth fetching in full. Without `v=2`, the response above would be `{"players": true, "settings": true, "playerCount": true, "active": true}`, with no way to tell which children are containers and which are primitives. ## Querying Filter and sort data using query parameters. These map directly to the query methods available in the SDKs. ### Order by Specify a sort order with `orderBy`: ```bash theme={null} # Order by a child key curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"' # Order by key curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="$key"' # Order by value (for lists of primitives) curl 'https://my-game--chess-app.larkdb.net/highscores.json?orderBy="$value"' # Order by priority curl 'https://my-game--chess-app.larkdb.net/tasks.json?orderBy="$priority"' ``` The `orderBy` value must be a JSON string. Wrap it in double quotes in the URL. Most shells require escaping: `orderBy=\"score\"` or `orderBy=%22score%22`. ### Limit results ```bash theme={null} # First 5 results curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"&limitToFirst=5' # Last 10 results curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"&limitToLast=10' ``` ### Range filters ```bash theme={null} # Players with score >= 100 curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"&startAt=100' # Players with score <= 500 curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"&endAt=500' # Players with score between 100 and 500 curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"&startAt=100&endAt=500' # Players with exactly 250 points curl 'https://my-game--chess-app.larkdb.net/players.json?orderBy="score"&equalTo=250' ``` Range values can be numbers, strings, or booleans. String values must be JSON-encoded (wrapped in quotes). ## Formatting ### Pretty print ```bash theme={null} curl 'https://my-game--chess-app.larkdb.net/players.json?print=pretty' ``` Returns indented, human-readable JSON. Useful for debugging. ### Silent response ```bash theme={null} curl -X PUT 'https://my-game--chess-app.larkdb.net/players/alice/score.json' \ -d '300' \ --header 'print: silent' ``` Or as a query parameter: ```bash theme={null} curl -X PUT 'https://my-game--chess-app.larkdb.net/players/alice/score.json?print=silent' \ -d '300' ``` Returns `204 No Content` instead of echoing the written data back. Saves bandwidth on writes when you don't need the response body. ### JSONP For cross-domain requests from browsers that don't support CORS: ```bash theme={null} curl 'https://my-game--chess-app.larkdb.net/players.json?callback=myFunc' ``` Response: ```javascript theme={null} myFunc({"alice":{"name":"Alice","score":250}}) ``` ### Download ```bash theme={null} curl 'https://my-game--chess-app.larkdb.net/players.json?download=players.json' ``` Sets the `Content-Disposition` header so browsers treat the response as a file download. ## Timeouts Set a request timeout with the `timeout` parameter: ```bash theme={null} # 10-second timeout curl 'https://my-game--chess-app.larkdb.net/big-dataset.json?timeout=10s' # 3-minute timeout curl 'https://my-game--chess-app.larkdb.net/big-dataset.json?timeout=3min' ``` Valid suffixes: `ms`, `s`, `min`. Maximum timeout is 15 minutes. Default is 30 seconds. If the request exceeds the timeout, you'll get a `504 Gateway Timeout` response. # Streaming Source: https://docs.larksh.com/rest-api/streaming Subscribe to real-time updates with Server-Sent Events The REST API supports Server-Sent Events (SSE) for real-time streaming. Send a `GET` request with `Accept: text/event-stream` and Lark pushes data changes to you as they happen. ## Starting a stream ```bash theme={null} curl -N \ -H 'Accept: text/event-stream' \ 'https://my-game--chess-app.larkdb.net/players.json?auth=YOUR_TOKEN' ``` The `-N` flag disables curl's output buffering so events appear immediately. ## Event format Events follow the [SSE specification](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). Each event has a type and a JSON data payload: ``` event: put data: {"path":"/","data":{"alice":{"name":"Alice","score":250},"bob":{"name":"Bob","score":180}}} event: patch data: {"path":"/alice/score","data":300} event: put data: {"path":"/carol","data":{"name":"Carol","score":0}} ``` ### Event types | Event | Description | | -------------- | -------------------------------------------------------------------------------------------------------------- | | `put` | Data at the path was replaced. The initial event is always a `put` with the full value at the subscribed path. | | `patch` | Data was partially updated. Only the changed fields are included. | | `keep-alive` | Sent periodically to keep the connection open. No data payload. | | `cancel` | The stream was terminated, typically because security rules revoked read access. | | `auth_revoked` | The auth token expired or was invalidated. | ### Initial data The first event on any stream is a `put` containing the complete current value at the path: ``` event: put data: {"path":"/","data":{"alice":{"name":"Alice","score":250},"bob":{"name":"Bob","score":180}}} ``` After that, you receive incremental `put` and `patch` events as data changes. ## Path semantics The `path` field in each event is relative to the path you subscribed to. If you stream `/players`: * `{"path":"/","data":{...}}` means the entire `/players` subtree changed. * `{"path":"/alice/score","data":300}` means only `/players/alice/score` changed. * `{"path":"/carol","data":{"name":"Carol","score":0}}` means a new child `/players/carol` was added. ## Using SSE in code ### Browser (EventSource) ```typescript theme={null} const url = 'https://my-game--chess-app.larkdb.net/players.json?auth=YOUR_TOKEN'; const source = new EventSource(url); source.addEventListener('put', (e) => { const { path, data } = JSON.parse(e.data); console.log('PUT at', path, data); }); source.addEventListener('patch', (e) => { const { path, data } = JSON.parse(e.data); console.log('PATCH at', path, data); }); source.addEventListener('cancel', () => { console.log('Stream cancelled — check security rules'); source.close(); }); ``` ### Node.js Use any SSE client library, or read the stream manually: ```typescript theme={null} const response = await fetch( 'https://my-game--chess-app.larkdb.net/players.json?auth=YOUR_TOKEN', { headers: { 'Accept': 'text/event-stream' } } ); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); // Parse SSE events from text console.log(text); } ``` ## When to use streaming SSE streaming is useful when you need real-time updates but can't use a WebSocket-based SDK: * Watching for changes from a backend service. * Streaming events to a log or monitoring pipeline. * Simple web clients where `EventSource` (built into every browser, no dependencies) is enough. For most client-side applications, the [Lark SDK](/lark-sdk/overview) or [Firebase SDKs](/firebase/overview) provide a better developer experience with automatic reconnection, local caching, and typed snapshots. # Writing data Source: https://docs.larksh.com/rest-api/writing Create, update, and delete data with PUT, POST, PATCH, and DELETE Write data by sending `PUT`, `POST`, `PATCH`, or `DELETE` requests. The request body is JSON. ## Set (PUT) Replace the data at a path completely. Any existing data at that path is overwritten. ```bash theme={null} curl -X PUT \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' \ -d '{"name": "Alice", "score": 250, "online": true}' ``` Response (echoes the written data): ```json theme={null} { "name": "Alice", "score": 250, "online": true } ``` `PUT` to a path that doesn't exist creates it. `PUT` to a path that does exist replaces it entirely. Missing fields are deleted, not preserved. ## Push (POST) Add a new child with an auto-generated key. This is the REST equivalent of `push()` in the SDKs. ```bash theme={null} curl -X POST \ 'https://my-game--chess-app.larkdb.net/messages.json?auth=YOUR_TOKEN' \ -d '{"text": "Hello!", "sender": "alice", "timestamp": {".sv": "timestamp"}}' ``` Response: ```json theme={null} { "name": "-Kabc123def" } ``` The `name` field contains the generated key. Push IDs are chronologically sortable, so later posts sort after earlier ones. Use `POST` for append-only data like chat messages, event logs, or queue items where each entry needs a unique key. ## Update (PATCH) Merge data into an existing path. Only the specified keys are modified; everything else is preserved. ```bash theme={null} curl -X PATCH \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' \ -d '{"score": 300, "lastSeen": {".sv": "timestamp"}}' ``` This updates Alice's `score` and `lastSeen` without touching `name` or `online`. ### Multi-path updates Update multiple locations atomically by using paths as keys in your `PATCH` body at a higher level: ```bash theme={null} curl -X PATCH \ 'https://my-game--chess-app.larkdb.net/.json?auth=YOUR_TOKEN' \ -d '{ "players/alice/score": 300, "players/bob/score": 275, "leaderboard/first": "alice", "leaderboard/second": "bob" }' ``` All four updates happen atomically. Either they all succeed or none do. Multi-path updates are the way to keep denormalized data consistent. ## Remove (DELETE) Delete data at a path. ```bash theme={null} curl -X DELETE \ 'https://my-game--chess-app.larkdb.net/players/alice.json?auth=YOUR_TOKEN' ``` Returns `null` on success. Deleting a non-existent path succeeds silently. ## Server values Use `{".sv": "timestamp"}` to write the server's current timestamp: ```bash theme={null} curl -X PUT \ 'https://my-game--chess-app.larkdb.net/players/alice/lastSeen.json?auth=YOUR_TOKEN' \ -d '{".sv": "timestamp"}' ``` The server replaces `{".sv": "timestamp"}` with the actual Unix timestamp in milliseconds. ## Silent writes If you don't need the response body (saving bandwidth), add `print=silent`: ```bash theme={null} curl -X PUT \ 'https://my-game--chess-app.larkdb.net/players/alice/score.json?auth=YOUR_TOKEN&print=silent' \ -d '500' ``` Returns `204 No Content` instead of echoing the written data. ## Error responses When a write fails, you get an error object: ```json theme={null} { "error": "permission_denied", "message": "You do not have permission to write to this path" } ``` | Status code | Meaning | | ----------- | ------------------------------------- | | `200` | Write succeeded | | `204` | Write succeeded (with `print=silent`) | | `400` | Invalid JSON or bad request | | `403` | Security rules denied the write | | `401` | Authentication required | # Supported platforms Source: https://docs.larksh.com/supported-platforms Every platform and SDK you can use to connect to Lark Lark supports two ways to connect: the **Lark SDK** (a lightweight, purpose-built client) and the **Firebase SDKs** (drop-in compatibility with your existing Firebase code). You can also use the **REST API** from any language or platform. ## Client SDKs | Platform | Lark SDK | Firebase SDK | | --------------------------- | --------------------------------- | ----------------------------------------- | | **JavaScript / TypeScript** | [Lark JS SDK](/lark-sdk/overview) | [Firebase JS SDK](/firebase/javascript) | | **Android** (Kotlin / Java) | — | [Firebase Android SDK](/firebase/android) | | **iOS / Apple** (Swift) | — | [Firebase iOS SDK](/firebase/ios) | | **Flutter** (Dart) | — | [Firebase Flutter SDK](/firebase/flutter) | | **C++** | — | [Firebase C++ SDK](/firebase/cpp) | | **Unity** (C#) | — | [Firebase Unity SDK](/firebase/unity) | ## REST API The [REST API](/rest-api/overview) works from any language or platform that can make HTTP requests: Python, Ruby, Go, Java, shell scripts, serverless functions, or anything else. It's 100% compatible with the Firebase Realtime Database REST API. ## How to choose * If you're building a JavaScript / TypeScript app, the [Lark SDK](/lark-sdk/overview) is the best choice. It's smaller (\~20KB), supports WebTransport for lower latency, and has built-in support for volatile paths. If you're migrating an existing Firebase app, the [Firebase JS SDK](/firebase/javascript) works too. Just change the URL. * For mobile or game engines, use the [Firebase SDK](/firebase/overview) for your platform. Change the database URL and everything works. * For server-side or scripting, use the [REST API](/rest-api/overview). No SDK needed. ## What's next Create a project and build your first real-time app in minutes. Understand Lark's data model, security rules, and real-time features.