diff --git a/docs/pages/concepts.mdx b/docs/pages/concepts.mdx
index df0e1bde891..45890445c37 100644
--- a/docs/pages/concepts.mdx
+++ b/docs/pages/concepts.mdx
@@ -117,26 +117,6 @@ forms, 3D files, video editors, design files, presentations, and more.
Primitives are the building blocks available inside rooms that make multiplayer
possible at scale: Presence, Broadcast, Storage, Feeds, Threads.
-
-
-
-
-
-
-
-
## Collaboration features
With Liveblocks, you can build all sorts of collaboration features. Most of
diff --git a/docs/pages/platform/management-api.mdx b/docs/pages/platform/management-api.mdx
index 476bc8b6e6e..633791fb32f 100644
--- a/docs/pages/platform/management-api.mdx
+++ b/docs/pages/platform/management-api.mdx
@@ -205,6 +205,21 @@ jobs:
-H "Content-Type: application/json"
```
+### Filter projects by name
+
+Use the optional `query` parameter with the `name` field to list projects whose
+name exactly matches the provided value. Matching is case-sensitive, and project
+names are not unique, so the response may contain multiple projects. Pagination
+applies to the filtered results.
+
+```bash
+# >_ terminal
+curl --get "https://api.liveblocks.io/v2/management/projects" \
+ -H "Authorization: Bearer lbs_*******************" \
+ -H "Content-Type: application/json" \
+ --data-urlencode 'query=name:"My Project"'
+```
+
## Rate limiting
The Management API enforces rate limits to ensure fair usage and system
diff --git a/docs/references/v2.openapi.json b/docs/references/v2.openapi.json
index 53ae17a1c24..86f5df93fea 100644
--- a/docs/references/v2.openapi.json
+++ b/docs/references/v2.openapi.json
@@ -6874,9 +6874,18 @@
"get": {
"summary": "List projects",
"x-badge": "Beta",
- "description": "Returns a paginated list of projects. You can limit the number of projects returned per page and use the provided `nextCursor` for pagination. This endpoint requires the `read:all` scope.",
+ "description": "Returns a paginated list of projects. You can filter by exact, case-sensitive project name using the `query` parameter. Project names are not unique, so multiple projects can match. You can limit the number of projects returned per page and use the provided `nextCursor` for pagination. This endpoint requires the `read:all` scope.",
"tags": ["Management"],
"parameters": [
+ {
+ "schema": {
+ "type": "string",
+ "description": "A query to filter projects by exact, case-sensitive name.",
+ "example": "name:\"Frimousse\""
+ },
+ "in": "query",
+ "name": "query"
+ },
{
"schema": {
"type": "integer",
diff --git a/examples/nextjs-3d-builder/package.json b/examples/nextjs-3d-builder/package.json
index 45ced9cfcd4..a007f9d265c 100644
--- a/examples/nextjs-3d-builder/package.json
+++ b/examples/nextjs-3d-builder/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-3d-builder/pages/_app.js b/examples/nextjs-3d-builder/pages/_app.js
index e78507835ac..d180dc77ca3 100644
--- a/examples/nextjs-3d-builder/pages/_app.js
+++ b/examples/nextjs-3d-builder/pages/_app.js
@@ -8,9 +8,10 @@ function App({ Component, pageProps }) {
const roomId = useExampleRoomdId("nextjs-3d-builder");
return (
-
+
Liveblocks
diff --git a/examples/nextjs-ai-elements-realtime/package-lock.json b/examples/nextjs-ai-elements-realtime/package-lock.json
index 0970df8a9ee..8e5118935df 100644
--- a/examples/nextjs-ai-elements-realtime/package-lock.json
+++ b/examples/nextjs-ai-elements-realtime/package-lock.json
@@ -6087,9 +6087,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "5.1.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
- "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
+ "version": "5.1.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
+ "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
"funding": [
{
"type": "github",
diff --git a/examples/nextjs-connection-status/README.md b/examples/nextjs-connection-status/README.md
index c77c2ae052e..445295b9c06 100644
--- a/examples/nextjs-connection-status/README.md
+++ b/examples/nextjs-connection-status/README.md
@@ -65,10 +65,11 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider`
-- Replace `publicApiKey` in `LiveblocksProvider` with `"pk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to
+ [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `pk_localdev`.
diff --git a/examples/nextjs-connection-status/app/Providers.tsx b/examples/nextjs-connection-status/app/Providers.tsx
index 16511d14c87..02ac0dd0c95 100644
--- a/examples/nextjs-connection-status/app/Providers.tsx
+++ b/examples/nextjs-connection-status/app/Providers.tsx
@@ -7,6 +7,7 @@ export function Providers({ children }: PropsWithChildren) {
return (
diff --git a/examples/nextjs-dashboard/package.json b/examples/nextjs-dashboard/package.json
index c0c82460c6d..e57ee027842 100644
--- a/examples/nextjs-dashboard/package.json
+++ b/examples/nextjs-dashboard/package.json
@@ -13,6 +13,7 @@
},
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-dashboard/pages/_app.tsx b/examples/nextjs-dashboard/pages/_app.tsx
index fd83241ca9a..4cf1b8e74a9 100644
--- a/examples/nextjs-dashboard/pages/_app.tsx
+++ b/examples/nextjs-dashboard/pages/_app.tsx
@@ -12,6 +12,7 @@ function App({ Component, pageProps }: AppProps) {
return (
diff --git a/examples/nextjs-form/package.json b/examples/nextjs-form/package.json
index 26ccf3144e4..de4fdb5c473 100644
--- a/examples/nextjs-form/package.json
+++ b/examples/nextjs-form/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-form/pages/_app.tsx b/examples/nextjs-form/pages/_app.tsx
index 6726f514cb8..e67198a5b4f 100644
--- a/examples/nextjs-form/pages/_app.tsx
+++ b/examples/nextjs-form/pages/_app.tsx
@@ -11,7 +11,10 @@ function App({ Component, pageProps }: AppProps) {
const roomId = useExampleRoomId("liveblocks:examples:nextjs-form");
return (
-
+
diff --git a/examples/nextjs-live-avatars-advanced/package.json b/examples/nextjs-live-avatars-advanced/package.json
index c76106fed4d..f7f6ea418cf 100644
--- a/examples/nextjs-live-avatars-advanced/package.json
+++ b/examples/nextjs-live-avatars-advanced/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-live-avatars-advanced/pages/_app.tsx b/examples/nextjs-live-avatars-advanced/pages/_app.tsx
index cf92a47aebd..7277be455ac 100644
--- a/examples/nextjs-live-avatars-advanced/pages/_app.tsx
+++ b/examples/nextjs-live-avatars-advanced/pages/_app.tsx
@@ -5,7 +5,10 @@ import Head from "next/head";
function MyApp({ Component, pageProps }: AppProps) {
return (
-
+
Liveblocks
diff --git a/examples/nextjs-live-avatars-advanced/pages/api/liveblocks-auth.ts b/examples/nextjs-live-avatars-advanced/pages/api/liveblocks-auth.ts
index 9c357b56c69..5f56841e7b2 100644
--- a/examples/nextjs-live-avatars-advanced/pages/api/liveblocks-auth.ts
+++ b/examples/nextjs-live-avatars-advanced/pages/api/liveblocks-auth.ts
@@ -8,6 +8,7 @@ import { NextApiRequest, NextApiResponse } from "next";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
diff --git a/examples/nextjs-live-avatars/README.md b/examples/nextjs-live-avatars/README.md
index 5a3a06f67a6..5bcfdcab6cf 100644
--- a/examples/nextjs-live-avatars/README.md
+++ b/examples/nextjs-live-avatars/README.md
@@ -60,11 +60,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-live-avatars/app/Providers.tsx b/examples/nextjs-live-avatars/app/Providers.tsx
index 7476ab86c39..8f93533afb2 100644
--- a/examples/nextjs-live-avatars/app/Providers.tsx
+++ b/examples/nextjs-live-avatars/app/Providers.tsx
@@ -7,6 +7,7 @@ export function Providers({ children }: PropsWithChildren) {
return (
{
const searchParams = new URLSearchParams(
userIds.map((userId) => ["userIds", userId])
diff --git a/examples/nextjs-live-avatars/app/api/liveblocks-auth/route.ts b/examples/nextjs-live-avatars/app/api/liveblocks-auth/route.ts
index 9e1188ddea6..0e5b88167f0 100644
--- a/examples/nextjs-live-avatars/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-live-avatars/app/api/liveblocks-auth/route.ts
@@ -10,6 +10,7 @@ import { getRandomUser, getUser } from "../../../database";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/examples/nextjs-live-avatars/package.json b/examples/nextjs-live-avatars/package.json
index e29de117838..db72c63f82e 100644
--- a/examples/nextjs-live-avatars/package.json
+++ b/examples/nextjs-live-avatars/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-live-cursors-advanced/README.md b/examples/nextjs-live-cursors-advanced/README.md
index f8544ccc9f3..e6ad5ddbd84 100644
--- a/examples/nextjs-live-cursors-advanced/README.md
+++ b/examples/nextjs-live-cursors-advanced/README.md
@@ -61,11 +61,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-live-cursors-advanced/package.json b/examples/nextjs-live-cursors-advanced/package.json
index bbd16cc2489..08cfe00e1e7 100644
--- a/examples/nextjs-live-cursors-advanced/package.json
+++ b/examples/nextjs-live-cursors-advanced/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-live-cursors-advanced/pages/_app.tsx b/examples/nextjs-live-cursors-advanced/pages/_app.tsx
index cf92a47aebd..7277be455ac 100644
--- a/examples/nextjs-live-cursors-advanced/pages/_app.tsx
+++ b/examples/nextjs-live-cursors-advanced/pages/_app.tsx
@@ -5,7 +5,10 @@ import Head from "next/head";
function MyApp({ Component, pageProps }: AppProps) {
return (
-
+
Liveblocks
diff --git a/examples/nextjs-live-cursors-advanced/pages/api/liveblocks-auth.ts b/examples/nextjs-live-cursors-advanced/pages/api/liveblocks-auth.ts
index dae46e09f42..c2d2b3a8032 100644
--- a/examples/nextjs-live-cursors-advanced/pages/api/liveblocks-auth.ts
+++ b/examples/nextjs-live-cursors-advanced/pages/api/liveblocks-auth.ts
@@ -8,6 +8,7 @@ import { NextApiRequest, NextApiResponse } from "next";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
diff --git a/examples/nextjs-live-cursors-chat/README.md b/examples/nextjs-live-cursors-chat/README.md
index 37f152ed0b5..211cd4f1817 100644
--- a/examples/nextjs-live-cursors-chat/README.md
+++ b/examples/nextjs-live-cursors-chat/README.md
@@ -60,10 +60,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider`
-- Replace `publicApiKey` in `LiveblocksProvider` with `"pk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `pk_localdev`.
diff --git a/examples/nextjs-live-cursors-chat/package.json b/examples/nextjs-live-cursors-chat/package.json
index b4466dcea45..c43b7dd64f3 100644
--- a/examples/nextjs-live-cursors-chat/package.json
+++ b/examples/nextjs-live-cursors-chat/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-live-cursors-chat/pages/_app.tsx b/examples/nextjs-live-cursors-chat/pages/_app.tsx
index 5debb8b184e..369a4056004 100644
--- a/examples/nextjs-live-cursors-chat/pages/_app.tsx
+++ b/examples/nextjs-live-cursors-chat/pages/_app.tsx
@@ -8,6 +8,7 @@ function App({ Component, pageProps }: AppProps) {
Liveblocks
diff --git a/examples/nextjs-live-cursors-scroll/README.md b/examples/nextjs-live-cursors-scroll/README.md
index 45de025013c..98891a2a4a1 100644
--- a/examples/nextjs-live-cursors-scroll/README.md
+++ b/examples/nextjs-live-cursors-scroll/README.md
@@ -60,10 +60,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider`
-- Replace `publicApiKey` in `LiveblocksProvider` with `"pk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `pk_localdev`.
diff --git a/examples/nextjs-live-cursors-scroll/package.json b/examples/nextjs-live-cursors-scroll/package.json
index e0cecd70ee2..d0387e2525a 100644
--- a/examples/nextjs-live-cursors-scroll/package.json
+++ b/examples/nextjs-live-cursors-scroll/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-live-cursors-scroll/pages/_app.tsx b/examples/nextjs-live-cursors-scroll/pages/_app.tsx
index 5d35a4a482f..a3536a2cac5 100644
--- a/examples/nextjs-live-cursors-scroll/pages/_app.tsx
+++ b/examples/nextjs-live-cursors-scroll/pages/_app.tsx
@@ -8,6 +8,7 @@ function App({ Component, pageProps }: AppProps) {
Liveblocks
diff --git a/examples/nextjs-live-cursors/README.md b/examples/nextjs-live-cursors/README.md
index bc45381adbb..62086119fc8 100644
--- a/examples/nextjs-live-cursors/README.md
+++ b/examples/nextjs-live-cursors/README.md
@@ -60,10 +60,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider`
-- Replace `publicApiKey` in `LiveblocksProvider` with `"pk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-live-cursors/app/Providers.tsx b/examples/nextjs-live-cursors/app/Providers.tsx
index 7476ab86c39..8f93533afb2 100644
--- a/examples/nextjs-live-cursors/app/Providers.tsx
+++ b/examples/nextjs-live-cursors/app/Providers.tsx
@@ -7,6 +7,7 @@ export function Providers({ children }: PropsWithChildren) {
return (
{
const searchParams = new URLSearchParams(
userIds.map((userId) => ["userIds", userId])
diff --git a/examples/nextjs-live-cursors/app/api/liveblocks-auth/route.ts b/examples/nextjs-live-cursors/app/api/liveblocks-auth/route.ts
index 9e1188ddea6..0e5b88167f0 100644
--- a/examples/nextjs-live-cursors/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-live-cursors/app/api/liveblocks-auth/route.ts
@@ -10,6 +10,7 @@ import { getRandomUser, getUser } from "../../../database";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/examples/nextjs-live-cursors/package.json b/examples/nextjs-live-cursors/package.json
index 032106b84e8..1d86e750b57 100644
--- a/examples/nextjs-live-cursors/package.json
+++ b/examples/nextjs-live-cursors/package.json
@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-live-form-selection/README.md b/examples/nextjs-live-form-selection/README.md
index fec6fa83738..2ab69b9e01e 100644
--- a/examples/nextjs-live-form-selection/README.md
+++ b/examples/nextjs-live-form-selection/README.md
@@ -60,10 +60,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider`
-- Replace `publicApiKey` in `LiveblocksProvider` with `"pk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `pk_localdev`.
diff --git a/examples/nextjs-live-form-selection/package.json b/examples/nextjs-live-form-selection/package.json
index f73eff67910..627ba085897 100644
--- a/examples/nextjs-live-form-selection/package.json
+++ b/examples/nextjs-live-form-selection/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-live-form-selection/pages/_app.tsx b/examples/nextjs-live-form-selection/pages/_app.tsx
index be2aae53875..e48c4a747c0 100644
--- a/examples/nextjs-live-form-selection/pages/_app.tsx
+++ b/examples/nextjs-live-form-selection/pages/_app.tsx
@@ -7,6 +7,7 @@ function App({ Component, pageProps }: AppProps) {
return (
Liveblocks
diff --git a/examples/nextjs-nextauth-google-avatars/README.md b/examples/nextjs-nextauth-google-avatars/README.md
index b3dd855fdd9..fa0a7f3d040 100644
--- a/examples/nextjs-nextauth-google-avatars/README.md
+++ b/examples/nextjs-nextauth-google-avatars/README.md
@@ -28,3 +28,14 @@ This example shows how to build a live avatar stack with [Liveblocks](https://li
## Getting started
Clone this repository, and then follow [the tutorial](https://liveblocks.io/blog/how-to-add-google-authentication-to-your-nextjs-liveblocks-app-with-nextauthjs) to add Google authentication.
+
+### Run with the local dev server
+
+After setting up the example, run:
+
+```bash
+npm run dev:local
+```
+
+This starts a local server and configures the Liveblocks client with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-nextauth-google-avatars/package.json b/examples/nextjs-nextauth-google-avatars/package.json
index 483434fd86d..820602ecd75 100644
--- a/examples/nextjs-nextauth-google-avatars/package.json
+++ b/examples/nextjs-nextauth-google-avatars/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start"
},
diff --git a/examples/nextjs-nextauth-google-avatars/pages/_app.tsx b/examples/nextjs-nextauth-google-avatars/pages/_app.tsx
index 0ec926759ec..f632c03f7ac 100644
--- a/examples/nextjs-nextauth-google-avatars/pages/_app.tsx
+++ b/examples/nextjs-nextauth-google-avatars/pages/_app.tsx
@@ -7,7 +7,10 @@ import { LiveblocksProvider } from "@liveblocks/react";
function App({ Component, pageProps }: AppProps<{ session: Session }>) {
return (
-
+
Liveblocks
diff --git a/examples/nextjs-nextauth-google-avatars/pages/api/liveblocks-auth.ts b/examples/nextjs-nextauth-google-avatars/pages/api/liveblocks-auth.ts
index 4c976224810..ea72599c3ca 100644
--- a/examples/nextjs-nextauth-google-avatars/pages/api/liveblocks-auth.ts
+++ b/examples/nextjs-nextauth-google-avatars/pages/api/liveblocks-auth.ts
@@ -10,6 +10,7 @@ import { User } from "../../types";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
@@ -33,10 +34,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
});
// Use a naming pattern to allow access to rooms with a wildcard
- liveblocksSession.allow(
- `liveblocks:examples:*`,
- ["*:write"]
- );
+ liveblocksSession.allow(`liveblocks:examples:*`, ["*:write"]);
const { status, body } = await liveblocksSession.authorize();
res.status(status).end(body);
diff --git a/examples/nextjs-react-flow/app/api/liveblocks-auth/route.ts b/examples/nextjs-react-flow/app/api/liveblocks-auth/route.ts
index 3a8f5756c9b..ac1d83d7ec0 100644
--- a/examples/nextjs-react-flow/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-react-flow/app/api/liveblocks-auth/route.ts
@@ -7,9 +7,10 @@ import { getRandomUser } from "../database";
* https://liveblocks.io/docs/authentication
*/
-const liveblocks = new Liveblocks({
- secret: process.env.LIVEBLOCKS_SECRET_KEY!,
-});
+ const liveblocks = new Liveblocks({
+ secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
+ });
export async function POST(_request: NextRequest) {
if (!process.env.LIVEBLOCKS_SECRET_KEY) {
diff --git a/examples/nextjs-react-flow/app/page.tsx b/examples/nextjs-react-flow/app/page.tsx
index 4e96371fe2e..b4a6b507a0b 100644
--- a/examples/nextjs-react-flow/app/page.tsx
+++ b/examples/nextjs-react-flow/app/page.tsx
@@ -16,6 +16,7 @@ export default function Page() {
{
const searchParams = new URLSearchParams(
userIds.map((userId) => ["userIds", userId])
diff --git a/examples/nextjs-react-flow/package.json b/examples/nextjs-react-flow/package.json
index a45f465c134..033830ac16c 100644
--- a/examples/nextjs-react-flow/package.json
+++ b/examples/nextjs-react-flow/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-tldraw-whiteboard-storage/package.json b/examples/nextjs-tldraw-whiteboard-storage/package.json
index 42698f9a4bf..5255399dc17 100644
--- a/examples/nextjs-tldraw-whiteboard-storage/package.json
+++ b/examples/nextjs-tldraw-whiteboard-storage/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-tldraw-whiteboard-yjs/package.json b/examples/nextjs-tldraw-whiteboard-yjs/package.json
index d2aa68bb57b..515903bd03c 100644
--- a/examples/nextjs-tldraw-whiteboard-yjs/package.json
+++ b/examples/nextjs-tldraw-whiteboard-yjs/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-tldraw-whiteboard-yjs/src/app/Room.tsx b/examples/nextjs-tldraw-whiteboard-yjs/src/app/Room.tsx
index 096546d8352..81708bd353b 100644
--- a/examples/nextjs-tldraw-whiteboard-yjs/src/app/Room.tsx
+++ b/examples/nextjs-tldraw-whiteboard-yjs/src/app/Room.tsx
@@ -13,6 +13,7 @@ export function Room({ children }: { children: ReactNode }) {
return (
Liveblocks
diff --git a/examples/nextjs-yjs-codemirror/README.md b/examples/nextjs-yjs-codemirror/README.md
index f26b95bdf4a..5c9472ee939 100644
--- a/examples/nextjs-yjs-codemirror/README.md
+++ b/examples/nextjs-yjs-codemirror/README.md
@@ -68,11 +68,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-yjs-codemirror/package.json b/examples/nextjs-yjs-codemirror/package.json
index 035965b8e06..9cec81fa4e8 100644
--- a/examples/nextjs-yjs-codemirror/package.json
+++ b/examples/nextjs-yjs-codemirror/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-yjs-codemirror/src/app/Providers.tsx b/examples/nextjs-yjs-codemirror/src/app/Providers.tsx
index 9a25c2f214d..5d1c1928ca2 100644
--- a/examples/nextjs-yjs-codemirror/src/app/Providers.tsx
+++ b/examples/nextjs-yjs-codemirror/src/app/Providers.tsx
@@ -5,7 +5,10 @@ import { PropsWithChildren, Suspense } from "react";
export function Providers({ children }: PropsWithChildren) {
return (
-
+
{children}
);
diff --git a/examples/nextjs-yjs-codemirror/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-yjs-codemirror/src/app/api/liveblocks-auth/route.ts
index f3ffb5c6547..6cc5b7b3530 100644
--- a/examples/nextjs-yjs-codemirror/src/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-yjs-codemirror/src/app/api/liveblocks-auth/route.ts
@@ -8,6 +8,7 @@ import { NextRequest } from "next/server";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/examples/nextjs-yjs-monaco/README.md b/examples/nextjs-yjs-monaco/README.md
index 7e8cc935aa3..49b88399ffb 100644
--- a/examples/nextjs-yjs-monaco/README.md
+++ b/examples/nextjs-yjs-monaco/README.md
@@ -73,11 +73,11 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to
+ [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-yjs-monaco/package.json b/examples/nextjs-yjs-monaco/package.json
index 5b967e14ff2..263eb0b45dd 100644
--- a/examples/nextjs-yjs-monaco/package.json
+++ b/examples/nextjs-yjs-monaco/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-yjs-monaco/src/app/Providers.tsx b/examples/nextjs-yjs-monaco/src/app/Providers.tsx
index 9a25c2f214d..5d1c1928ca2 100644
--- a/examples/nextjs-yjs-monaco/src/app/Providers.tsx
+++ b/examples/nextjs-yjs-monaco/src/app/Providers.tsx
@@ -5,7 +5,10 @@ import { PropsWithChildren, Suspense } from "react";
export function Providers({ children }: PropsWithChildren) {
return (
-
+
{children}
);
diff --git a/examples/nextjs-yjs-monaco/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-yjs-monaco/src/app/api/liveblocks-auth/route.ts
index f3ffb5c6547..6cc5b7b3530 100644
--- a/examples/nextjs-yjs-monaco/src/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-yjs-monaco/src/app/api/liveblocks-auth/route.ts
@@ -8,6 +8,7 @@ import { NextRequest } from "next/server";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/examples/nextjs-yjs-quill/README.md b/examples/nextjs-yjs-quill/README.md
index fa2292cc91a..186bd59cc0c 100644
--- a/examples/nextjs-yjs-quill/README.md
+++ b/examples/nextjs-yjs-quill/README.md
@@ -72,11 +72,11 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to
+ [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-yjs-quill/package.json b/examples/nextjs-yjs-quill/package.json
index 9d1f10536b1..a8926945243 100644
--- a/examples/nextjs-yjs-quill/package.json
+++ b/examples/nextjs-yjs-quill/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-yjs-quill/src/app/Providers.tsx b/examples/nextjs-yjs-quill/src/app/Providers.tsx
index 9a25c2f214d..5d1c1928ca2 100644
--- a/examples/nextjs-yjs-quill/src/app/Providers.tsx
+++ b/examples/nextjs-yjs-quill/src/app/Providers.tsx
@@ -5,7 +5,10 @@ import { PropsWithChildren, Suspense } from "react";
export function Providers({ children }: PropsWithChildren) {
return (
-
+
{children}
);
diff --git a/examples/nextjs-yjs-quill/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-yjs-quill/src/app/api/liveblocks-auth/route.ts
index f3ffb5c6547..6cc5b7b3530 100644
--- a/examples/nextjs-yjs-quill/src/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-yjs-quill/src/app/api/liveblocks-auth/route.ts
@@ -8,6 +8,7 @@ import { NextRequest } from "next/server";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/examples/nextjs-yjs-slate/README.md b/examples/nextjs-yjs-slate/README.md
index 9fd44d774c5..61ea0ebd147 100644
--- a/examples/nextjs-yjs-slate/README.md
+++ b/examples/nextjs-yjs-slate/README.md
@@ -64,11 +64,10 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-yjs-slate/package.json b/examples/nextjs-yjs-slate/package.json
index 3a7ba059536..e6163038385 100644
--- a/examples/nextjs-yjs-slate/package.json
+++ b/examples/nextjs-yjs-slate/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-yjs-slate/src/app/Providers.tsx b/examples/nextjs-yjs-slate/src/app/Providers.tsx
index 9a25c2f214d..5d1c1928ca2 100644
--- a/examples/nextjs-yjs-slate/src/app/Providers.tsx
+++ b/examples/nextjs-yjs-slate/src/app/Providers.tsx
@@ -5,7 +5,10 @@ import { PropsWithChildren, Suspense } from "react";
export function Providers({ children }: PropsWithChildren) {
return (
-
+
{children}
);
diff --git a/examples/nextjs-yjs-slate/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-yjs-slate/src/app/api/liveblocks-auth/route.ts
index f3ffb5c6547..6cc5b7b3530 100644
--- a/examples/nextjs-yjs-slate/src/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-yjs-slate/src/app/api/liveblocks-auth/route.ts
@@ -8,6 +8,7 @@ import { NextRequest } from "next/server";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/examples/nextjs-yjs-superdoc/README.md b/examples/nextjs-yjs-superdoc/README.md
index 49496203148..39cdd9983c8 100644
--- a/examples/nextjs-yjs-superdoc/README.md
+++ b/examples/nextjs-yjs-superdoc/README.md
@@ -72,11 +72,11 @@ You can optionally run this example locally using the
[Liveblocks dev server](https://liveblocks.io/docs/tools/dev-server).
- Install the example as detailed above
-- Run `npx liveblocks dev` to start the server
-- Add `baseUrl: "http://localhost:1153"` option to `LiveblocksProvider` and
- `new Liveblocks`
-- Replace `secret` in `new Liveblocks` with `"sk_localdev"`
-- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000)
+- Run `npm run dev:local` and go to
+ [http://localhost:3000](http://localhost:3000)
+
+This starts a local server and configures the example with
+`http://localhost:1153` and `sk_localdev`.
diff --git a/examples/nextjs-yjs-superdoc/package.json b/examples/nextjs-yjs-superdoc/package.json
index 46ba8803c90..dd2f950ce7f 100644
--- a/examples/nextjs-yjs-superdoc/package.json
+++ b/examples/nextjs-yjs-superdoc/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "dev:local": "npx liveblocks dev --persist --cmd 'next dev'",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/examples/nextjs-yjs-superdoc/src/app/Providers.tsx b/examples/nextjs-yjs-superdoc/src/app/Providers.tsx
index 7323246b03f..31a6e4cdf5f 100644
--- a/examples/nextjs-yjs-superdoc/src/app/Providers.tsx
+++ b/examples/nextjs-yjs-superdoc/src/app/Providers.tsx
@@ -8,6 +8,7 @@ export function Providers({ children }: PropsWithChildren) {
return (
{
const searchParams = new URLSearchParams(
diff --git a/examples/nextjs-yjs-superdoc/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-yjs-superdoc/src/app/api/liveblocks-auth/route.ts
index 50b6d997865..2ccc1df1e98 100644
--- a/examples/nextjs-yjs-superdoc/src/app/api/liveblocks-auth/route.ts
+++ b/examples/nextjs-yjs-superdoc/src/app/api/liveblocks-auth/route.ts
@@ -9,6 +9,7 @@ import { NextRequest, NextResponse } from "next/server";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
+ baseUrl: process.env.NEXT_PUBLIC_LIVEBLOCKS_BASE_URL,
});
export async function POST(request: NextRequest) {
diff --git a/packages/liveblocks-server/CHANGELOG.md b/packages/liveblocks-server/CHANGELOG.md
index d25e5c1998d..bbfd4586330 100644
--- a/packages/liveblocks-server/CHANGELOG.md
+++ b/packages/liveblocks-server/CHANGELOG.md
@@ -1,5 +1,19 @@
## vNEXT (not yet released)
+## v1.9.0
+
+- Add `--persist` / `--no-persist` to `liveblocks dev`, making storage an
+ explicit choice. `liveblocks dev --cmd` still defaults to using a throwaway
+ directory (`--no-persist` is implied by default), but you can pass `--persist`
+ to keep data between runs, which is useful when `--cmd` runs your app rather
+ than your tests.
+- Running `liveblocks dev --cmd ...` will now set the following env variables in
+ the environment: `LIVEBLOCKS_BASE_URL`, `LIVEBLOCKS_PUBLIC_KEY`,
+ `LIVEBLOCKS_SECRET_KEY`, and any `NEXT_PUBLIC_`, `VITE_`, and `PUBLIC_`
+ prefixed variants for client-side code.
+- Remove `--ci` from `liveblocks dev`. It bundled a storage choice with a check
+ choice; use `--no-persist --no-check` instead.
+
## v1.8.0
- Add full support for LiveFile
diff --git a/packages/liveblocks-server/package.json b/packages/liveblocks-server/package.json
index 54621d881e7..974f65519a0 100644
--- a/packages/liveblocks-server/package.json
+++ b/packages/liveblocks-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/server",
- "version": "1.8.1-pre1",
+ "version": "1.9.0",
"description": "Liveblocks backend server foundation.",
"type": "module",
"main": "./dist/index.js",
diff --git a/starter-kits/nextjs-starter-kit/package-lock.json b/starter-kits/nextjs-starter-kit/package-lock.json
index fec13cb3c89..4f0b96fa23d 100644
--- a/starter-kits/nextjs-starter-kit/package-lock.json
+++ b/starter-kits/nextjs-starter-kit/package-lock.json
@@ -8779,9 +8779,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
- "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+ "version": "5.1.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
+ "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
"funding": [
{
"type": "github",
diff --git a/tools/liveblocks-cli/Dockerfile b/tools/liveblocks-cli/Dockerfile
index 7ecb405f735..fc70155c76f 100644
--- a/tools/liveblocks-cli/Dockerfile
+++ b/tools/liveblocks-cli/Dockerfile
@@ -36,4 +36,4 @@ HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["bun", "run", "scripts/docker-healthcheck.ts"]
ENTRYPOINT ["bun", "run", "node_modules/liveblocks/dist/index.js"]
-CMD ["dev", "--ci"]
+CMD ["dev", "--no-persist", "--no-check"]
diff --git a/tools/liveblocks-cli/README.md b/tools/liveblocks-cli/README.md
index 6b713ec0279..6fba6f830bc 100644
--- a/tools/liveblocks-cli/README.md
+++ b/tools/liveblocks-cli/README.md
@@ -19,8 +19,9 @@ Options:
| `--port`, `-p ` | Port to listen on. | `1153` |
| `--random-port`, `-P` | Bind a random free port. The chosen port is exposed via the `LIVEBLOCKS_DEV_SERVER_PORT` env var. Ideal for CI. | |
| `--host ` | Host to bind to. | `localhost` |
-| `--cmd`, `-c ` | Run a one-off command against a fresh server instance, then shut down. Does not affect your local data in `.liveblocks/`. | |
-| `--ci` | Start a fresh server instance on every boot, ideal for CI. | |
+| `--cmd`, `-c ` | Run a command against the server, then shut down. The command inherits `LIVEBLOCKS_BASE_URL` and the local keys (also `NEXT_PUBLIC_`, `VITE_` and `PUBLIC_` prefixed), so apps need no source or `.env` changes. | |
+| `--persist` | Keep data in `.liveblocks/`. | Without `--cmd` |
+| `--no-persist` | Use a throwaway directory, leaving `.liveblocks/` untouched. | With `--cmd` |
| `--no-check` | Skip project setup check on start. | Checks by default |
| `--verbose`, `-v` | Show verbose output. | |
| `--help`, `-h` | Show help. | |
diff --git a/tools/liveblocks-cli/package.json b/tools/liveblocks-cli/package.json
index 3c82e9bb2cb..bf362659593 100644
--- a/tools/liveblocks-cli/package.json
+++ b/tools/liveblocks-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "liveblocks",
- "version": "1.8.1-pre1",
+ "version": "1.9.0",
"description": "Liveblocks command line interface",
"type": "module",
"bin": {
diff --git a/tools/liveblocks-cli/src/dev-server/db/rooms.ts b/tools/liveblocks-cli/src/dev-server/db/rooms.ts
index c54f9692ecf..f5e646f1758 100644
--- a/tools/liveblocks-cli/src/dev-server/db/rooms.ts
+++ b/tools/liveblocks-cli/src/dev-server/db/rooms.ts
@@ -55,7 +55,8 @@ export type ClientMeta = JsonObject;
// Module state
// ---------------------------------------------------------------------------
-const DEFAULT_BASE_PATH = ".liveblocks/v2";
+const DEFAULT_ROOT = ".liveblocks";
+const DEFAULT_BASE_PATH = join(DEFAULT_ROOT, "v2");
let basePath = DEFAULT_BASE_PATH;
let isEphemeral = false;
let _initializedDb: Database | null = null;
@@ -371,6 +372,20 @@ export function useEphemeralStorage(): string {
return root;
}
+/**
+ * Switch to persistent storage under `.liveblocks/`. Returns the root
+ * directory, the counterpart of `useEphemeralStorage()`'s return value, so
+ * callers can place sibling files (e.g. server.log) without caring which
+ * mode is active.
+ */
+export function usePersistentStorage(): string {
+ basePath = DEFAULT_BASE_PATH;
+ isEphemeral = false;
+ setBlobsRoot(blobsDir());
+ mkdirSync(DEFAULT_ROOT, { recursive: true });
+ return DEFAULT_ROOT;
+}
+
/**
* Get a room record from the DB. Returns undefined if not found.
*/
diff --git a/tools/liveblocks-cli/src/dev-server/index.ts b/tools/liveblocks-cli/src/dev-server/index.ts
index 3d78faf9976..5aca0edb125 100644
--- a/tools/liveblocks-cli/src/dev-server/index.ts
+++ b/tools/liveblocks-cli/src/dev-server/index.ts
@@ -45,6 +45,7 @@ import {
buildFixPrompt,
checkLiveblocksSetup,
} from "./lib/check-liveblocks-setup";
+import { childEnv } from "./lib/child-env";
import { copyToClipboard } from "./lib/clipboard";
import { isPortInUse } from "./lib/probe-port";
import { warn } from "./lib/xwarn";
@@ -107,6 +108,12 @@ function parsePort(value: string | undefined): number | undefined {
return Number.isInteger(n) && n > 0 && n <= 65535 ? n : undefined;
}
+/**
+ * Diagnostics go to stderr: with --cmd, stdout belongs to the command being
+ * run, and callers pipe it.
+ */
+const stderr = (msg: string) => process.stderr.write(msg + "\n");
+
function shellCmd(cmd: string): string[] {
return process.platform === "win32"
? [process.env.COMSPEC || "cmd.exe", "/c", cmd]
@@ -128,7 +135,8 @@ type Options = {
cmd?: string;
help: boolean;
"no-check": boolean;
- ci: boolean;
+ persist: boolean;
+ "no-persist": boolean;
verbose: boolean;
};
@@ -145,7 +153,8 @@ const dev: SubCommand = {
cmd: { type: "string", short: "c" },
help: { type: "boolean", short: "h", default: false },
"no-check": { type: "boolean", default: false },
- ci: { type: "boolean", default: false },
+ persist: { type: "boolean", default: false },
+ "no-persist": { type: "boolean", default: false },
verbose: { type: "boolean", short: "v", default: false },
},
{ allowPositionals: true }
@@ -171,38 +180,57 @@ const dev: SubCommand = {
console.log("Start the local Liveblocks dev server");
console.log();
console.log("Options:");
- console.log(` --port, -p Port to listen on (default: ${DEFAULT_PORT})`); // prettier-ignore
- console.log(" --random-port, -P Bind a random free port instead of --port (no collisions,"); // prettier-ignore
- console.log(" ever). With --cmd, the chosen port is exposed to the command"); // prettier-ignore
- console.log(" via LIVEBLOCKS_DEV_SERVER_PORT. Ideal for CI."); // prettier-ignore
- console.log(" --host Host to bind to (default: localhost)");
- console.log(" --cmd, -c Run a one-off command against a fresh server instance, then"); // prettier-ignore
- console.log(" shut down. Does not affect your local data in .liveblocks/."); // prettier-ignore
- console.log(" Extra args are appended to the command, or replace {} if"); // prettier-ignore
- console.log(" present. Use -- before args starting with -."); // prettier-ignore
- console.log(" --ci Start a fresh server instance on every boot, ideal for CI"); // prettier-ignore
- console.log(" --no-check Skip project setup check on start");
- console.log(" --verbose, -v Show verbose output");
- console.log(" --help, -h Show help");
+ console.log(` -p, --port Port to listen on (default: ${DEFAULT_PORT})`); // prettier-ignore
+ console.log(" -P, --random-port Bind a random free port instead, never collides"); // prettier-ignore
+ console.log(
+ " --host Host to bind to (default: localhost)"
+ );
+ console.log();
+ console.log(" -c, --cmd Run a command against the server, then shut down."); // prettier-ignore
+ console.log(" Extra args are appended, or replace {} if present;"); // prettier-ignore
+ console.log(" use -- before args starting with -. The command"); // prettier-ignore
+ console.log(" inherits LIVEBLOCKS_BASE_URL and the local keys"); // prettier-ignore
+ console.log(" (also NEXT_PUBLIC_, VITE_ and PUBLIC_ prefixed),"); // prettier-ignore
+ console.log(" so apps need no source or .env changes."); // prettier-ignore
+ console.log();
+ console.log(" --persist Keep data in .liveblocks/ (default without --cmd)"); // prettier-ignore
+ console.log(" --no-persist Use a throwaway directory (default with --cmd)"); // prettier-ignore
+ console.log();
+ console.log(
+ " --no-check Skip the project setup check on start"
+ );
+ console.log(" -v, --verbose Show verbose output");
+ console.log(" -h, --help Show this help");
return;
}
- let ephemeral = false;
-
- // --ci implies ephemeral + --no-check
- if (options.ci) {
- ephemeral = true;
+ // --cmd runs a command, then exits. The setup check is for interactive
+ // use, so it would only add noise here.
+ if (options.cmd) {
options["no-check"] = true;
}
- // --cmd implies ephemeral + --no-check
- if (options.cmd) {
- // NOTE: While this is CURRENTLY the same as --ci, we keep it separate in
- // case we want to have different implications here in the future
- ephemeral = true;
- options["no-check"] = true;
+ if (options.persist && options["no-persist"]) {
+ console.error(red("--persist and --no-persist are mutually exclusive"));
+ process.exit(1);
}
+ // Whether the server keeps its data in `.liveblocks/` rather than a
+ // throwaway directory.
+ //
+ // Persistence is its own axis, independent of whether a command is run and
+ // of whether the setup check runs. The default follows from who owns the
+ // server's lifetime: with `--cmd` the command does, so its data dies with
+ // it and a test run can never disturb `.liveblocks/`. Without `--cmd` a
+ // human does, so the data outlives the process. Either default can be
+ // stated explicitly, which is the only way to persist across `--cmd` runs
+ // or to get a throwaway server without one.
+ const persist = options.persist
+ ? true
+ : options["no-persist"]
+ ? false
+ : !options.cmd;
+
// With --random-port, bind to port 0 so the OS hands us a guaranteed-free
// port at bind time (no collisions, ever). Otherwise the precedence is:
// CLI flag > env var > default. The actually-bound port is read back from
@@ -215,7 +243,10 @@ const dev: SubCommand = {
const hostname =
options.host || process.env.LIVEBLOCKS_DEVSERVER_HOST || "localhost";
- const ephemeralPath = ephemeral ? Rooms.useEphemeralStorage() : null;
+ const storageRoot = persist
+ ? Rooms.usePersistentStorage()
+ : Rooms.useEphemeralStorage();
+ const storageLabel = persist ? "persistent" : "ephemeral";
if (requestedPort !== 0 && (await isPortInUse(requestedPort, hostname))) {
console.error(
@@ -406,17 +437,15 @@ const dev: SubCommand = {
// -----------------------------------------------------------------------------
- const stderr = (msg: string) => process.stderr.write(msg + "\n");
-
if (options.cmd) {
stderr(
`Liveblocks dev server ${dim(`v${__VERSION__}`)} running at http://${server.hostname}:${server.port}`
);
- if (ephemeralPath && options.verbose) {
- stderr(dim(`Ephemeral mode, using ${ephemeralPath}`));
+ if (options.verbose) {
+ stderr(dim(`${storageLabel} storage, using ${storageRoot}`));
}
// Redirect all further console output to a log file
- const logPath = join(ephemeralPath!, "server.log");
+ const logPath = join(storageRoot, "server.log");
stderr(dim(`Server logs: ${logPath}`));
const logFile = Bun.file(logPath).writer();
@@ -427,6 +456,13 @@ const dev: SubCommand = {
console.log = writeLine;
console.error = writeLine;
+ // A listening server always has a port; Bun's type just doesn't say so.
+ const boundPort = server.port;
+ if (boundPort === undefined) {
+ stderr(red("Dev server is not listening on a port"));
+ process.exit(1);
+ }
+
// Spawn child process, then shut down on exit
let code = 1;
try {
@@ -436,8 +472,7 @@ const dev: SubCommand = {
stderr: "inherit",
env: {
...process.env,
- LIVEBLOCKS_DEV_SERVER_HOST: hostname,
- LIVEBLOCKS_DEV_SERVER_PORT: String(server.port),
+ ...childEnv(hostname, boundPort),
},
});
@@ -677,8 +712,8 @@ const dev: SubCommand = {
originalLog(renderTabBar());
originalLog(logsLegend());
originalLog();
- if (ephemeralPath && options.verbose) {
- console.log(dim(`Ephemeral mode, using ${ephemeralPath}`));
+ if (options.verbose) {
+ console.log(dim(`${storageLabel} storage, using ${storageRoot}`));
}
process.stdin.setRawMode?.(true);
diff --git a/tools/liveblocks-cli/src/dev-server/lib/child-env.ts b/tools/liveblocks-cli/src/dev-server/lib/child-env.ts
new file mode 100644
index 00000000000..db988fb8142
--- /dev/null
+++ b/tools/liveblocks-cli/src/dev-server/lib/child-env.ts
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) Liveblocks Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export const LOCAL_PUBLIC_KEY = "pk_localdev";
+export const LOCAL_SECRET_KEY = "sk_localdev";
+
+/**
+ * Environment variables handed to `--cmd` child processes.
+ *
+ * An app that reads its Liveblocks connection details from the environment
+ * therefore talks to this dev server without any edit to its source or its
+ * `.env` files. That is the point: the same working tree runs against the
+ * cloud or against the dev server, with nothing to stage, stash or revert.
+ *
+ * These values intentionally take precedence over inherited ones. A parent
+ * environment holding real cloud credentials is the common case, and those
+ * credentials are useless here: the dev server only accepts its own local
+ * keys. Passing a real secret through would fail confusingly.
+ *
+ * Bundlers only expose variables carrying their own prefix to client code, so
+ * the client-side names are listed per framework rather than derived: Next.js
+ * `NEXT_PUBLIC_`, Vite `VITE_`, SvelteKit `PUBLIC_`. That same rule is why no
+ * secret appears under any of them, since such a name means "ship me to the
+ * browser". Server-side code reads the unprefixed `LIVEBLOCKS_SECRET_KEY`.
+ *
+ * Nuxt needs no prefixed entry: `nuxt.config.ts` runs in Node, so it reads the
+ * unprefixed names and forwards what it wants through `runtimeConfig.public`.
+ */
+export function childEnv(
+ hostname: string,
+ port: number
+): Record {
+ const baseUrl = `http://${hostname}:${port}`;
+
+ return {
+ LIVEBLOCKS_DEV_SERVER_HOST: hostname,
+ LIVEBLOCKS_DEV_SERVER_PORT: String(port),
+
+ LIVEBLOCKS_BASE_URL: baseUrl,
+ LIVEBLOCKS_PUBLIC_KEY: LOCAL_PUBLIC_KEY,
+ LIVEBLOCKS_SECRET_KEY: LOCAL_SECRET_KEY,
+
+ NEXT_PUBLIC_LIVEBLOCKS_BASE_URL: baseUrl,
+ NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY: LOCAL_PUBLIC_KEY,
+
+ VITE_LIVEBLOCKS_BASE_URL: baseUrl,
+ VITE_LIVEBLOCKS_PUBLIC_KEY: LOCAL_PUBLIC_KEY,
+
+ PUBLIC_LIVEBLOCKS_BASE_URL: baseUrl,
+ PUBLIC_LIVEBLOCKS_PUBLIC_KEY: LOCAL_PUBLIC_KEY,
+ };
+}
diff --git a/tools/liveblocks-cli/test/devserver/cmd.test.ts b/tools/liveblocks-cli/test/devserver/cmd.test.ts
index fa080576851..b17c87cbe14 100644
--- a/tools/liveblocks-cli/test/devserver/cmd.test.ts
+++ b/tools/liveblocks-cli/test/devserver/cmd.test.ts
@@ -15,8 +15,9 @@
* along with this program. If not, see .
*/
-import { existsSync } from "node:fs";
-import { resolve } from "node:path";
+import { existsSync, mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
import { beforeAll, describe, expect, test } from "bun:test";
@@ -30,10 +31,10 @@ const CWD = resolve(import.meta.dir, "../..");
*/
async function runCli(
subArgs: string[],
- options?: { env?: Record }
+ options?: { env?: Record; cwd?: string }
) {
const proc = Bun.spawn(["bun", CLI, ...subArgs], {
- cwd: CWD,
+ cwd: options?.cwd ?? CWD,
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, ...options?.env },
@@ -50,7 +51,7 @@ async function runCli(
async function runDevCommand(
args: string[],
- options?: { env?: Record }
+ options?: { env?: Record; cwd?: string }
) {
return runCli(["dev", ...args], options);
}
@@ -146,6 +147,81 @@ describe("liveblocks dev -c", () => {
expect(port).not.toBe(1153);
});
+ test("injects connection details so apps need no config changes", async () => {
+ const { stdout, exitCode } = await runDevCommand([
+ "-p",
+ "7780",
+ "-c",
+ "env | grep LIVEBLOCKS",
+ ]);
+ expect(exitCode).toBe(0);
+ expect(stdout).toContain("LIVEBLOCKS_BASE_URL=http://localhost:7780");
+ expect(stdout).toContain("LIVEBLOCKS_PUBLIC_KEY=pk_localdev");
+ expect(stdout).toContain("LIVEBLOCKS_SECRET_KEY=sk_localdev");
+ expect(stdout).toContain(
+ "NEXT_PUBLIC_LIVEBLOCKS_BASE_URL=http://localhost:7780"
+ );
+ expect(stdout).toContain("NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=pk_localdev");
+ expect(stdout).toContain("VITE_LIVEBLOCKS_BASE_URL=http://localhost:7780");
+ expect(stdout).toContain("VITE_LIVEBLOCKS_PUBLIC_KEY=pk_localdev");
+ expect(stdout).toContain(
+ "PUBLIC_LIVEBLOCKS_BASE_URL=http://localhost:7780"
+ );
+ expect(stdout).toContain("PUBLIC_LIVEBLOCKS_PUBLIC_KEY=pk_localdev");
+ });
+
+ test("never publishes a secret key under a client-visible prefix", async () => {
+ const { stdout, exitCode } = await runDevCommand([
+ "-p",
+ "7781",
+ "-c",
+ "env | grep LIVEBLOCKS",
+ ]);
+ expect(exitCode).toBe(0);
+
+ // Bundlers ship any variable carrying their own prefix to the browser, so
+ // a secret must never be injected under one. `VITE_` is Vite's equivalent
+ // of `NEXT_PUBLIC_`, not of `NEXT_`.
+ const published = stdout
+ .split("\n")
+ .map((line) => line.split("=")[0])
+ .filter((name) => /^(NEXT_PUBLIC_|VITE_|PUBLIC_)/.test(name));
+ expect(published).not.toContainEqual(expect.stringContaining("SECRET"));
+ });
+
+ test("overrides cloud credentials inherited from the parent environment", async () => {
+ const { stdout, exitCode } = await runDevCommand(
+ ["-p", "7782", "-c", "env | grep LIVEBLOCKS"],
+ {
+ env: {
+ LIVEBLOCKS_SECRET_KEY: "sk_prod_realkey",
+ NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY: "pk_prod_realkey",
+ },
+ }
+ );
+ expect(exitCode).toBe(0);
+ expect(stdout).not.toContain("sk_prod_realkey");
+ expect(stdout).not.toContain("pk_prod_realkey");
+ expect(stdout).toContain("LIVEBLOCKS_SECRET_KEY=sk_localdev");
+ expect(stdout).toContain("NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=pk_localdev");
+ });
+
+ test("injected base URL follows --random-port", async () => {
+ const { stdout, exitCode } = await runDevCommand([
+ "-P",
+ "-c",
+ "env | grep LIVEBLOCKS",
+ ]);
+ expect(exitCode).toBe(0);
+
+ const port = stdout.match(/LIVEBLOCKS_DEV_SERVER_PORT=(\d+)/)?.[1];
+ expect(port).toBeDefined();
+ expect(stdout).toContain(`LIVEBLOCKS_BASE_URL=http://localhost:${port}`);
+ expect(stdout).toContain(
+ `NEXT_PUBLIC_LIVEBLOCKS_BASE_URL=http://localhost:${port}`
+ );
+ });
+
test("does not change cwd of child process", async () => {
const { stdout, exitCode } = await runDevCommand([
"-p",
@@ -180,3 +256,47 @@ describe("liveblocks dev -c", () => {
expect(exitCode).toBe(42);
});
});
+
+describe("storage mode", () => {
+ test("--cmd is ephemeral by default, leaving .liveblocks/ alone", async () => {
+ const cwd = mkdtempSync(join(tmpdir(), "lb-cwd-"));
+ try {
+ const { stderr, exitCode } = await runDevCommand(
+ ["-P", "-v", "-c", "true"],
+ { cwd }
+ );
+ expect(exitCode).toBe(0);
+ expect(stderr).toContain("ephemeral storage");
+ expect(existsSync(join(cwd, ".liveblocks"))).toBe(false);
+ } finally {
+ rmSync(cwd, { recursive: true, force: true });
+ }
+ });
+
+ test("--persist keeps data under .liveblocks/", async () => {
+ const cwd = mkdtempSync(join(tmpdir(), "lb-cwd-"));
+ try {
+ const { stderr, exitCode } = await runDevCommand(
+ ["-P", "-v", "--persist", "-c", "true"],
+ { cwd }
+ );
+ expect(exitCode).toBe(0);
+ expect(stderr).toContain("persistent storage");
+ expect(existsSync(join(cwd, ".liveblocks"))).toBe(true);
+ } finally {
+ rmSync(cwd, { recursive: true, force: true });
+ }
+ });
+
+ test("--persist and --no-persist together is an error", async () => {
+ const { stderr, exitCode } = await runDevCommand([
+ "-P",
+ "--persist",
+ "--no-persist",
+ "-c",
+ "true",
+ ]);
+ expect(exitCode).toBe(1);
+ expect(stderr).toContain("mutually exclusive");
+ });
+});