A full collaborative-documents app built with Next.js (App Router) and Velt — like a minimal Google Docs. Sign in, see your documents, create new ones, share them with teammates (viewer / editor), and edit together in real time.
It demonstrates the core Velt features inside a realistic, multi-user app with working access control:
- Real-time editing (CRDT) — a TipTap editor synced with Yjs
- Comments — inline comments on selected text + a comments sidebar
- Presence & live cursors — see who's online and where their cursor is
- Notifications — in-app panel for mentions, replies and shares
- Sharing & permissions — restricted documents with per-user viewer/editor roles, enforced by Velt
Tech stack
- Next.js 16 (App Router) · React 19 · TypeScript
- Velt SDK (
@veltdev/react) + TipTap CRDT & Comments packages - Tailwind CSS v4
Deploy to Vercel
Click the Deploy with Vercel button above. The clone flow will prompt for the
two required Velt environment variables (NEXT_PUBLIC_VELT_API_KEY,
VELT_AUTH_TOKEN) and deploy the tiptap/ directory.
Then add a storage backend so the document directory persists (Vercel's runtime filesystem is read-only, so the local JSON store can't be used in production):
- In your Vercel project: Storage → Create Database → Upstash for Redis and
Connect to Project. Vercel injects
KV_REST_API_URL+KV_REST_API_TOKENautomatically (the variableslib/documentStore.tsreads). - Redeploy.
See The document directory: local dev vs. hosting for details.
Getting started
1. Get your Velt credentials
Create an app at console.velt.dev and copy your API key and Auth Token.
2. Configure environment variables
cp .env.example .env
# .envNEXT_PUBLIC_VELT_API_KEY=your_velt_api_key_here # public, used in the browserVELT_AUTH_TOKEN=your_velt_auth_token_here # secret, server-only (token + REST calls)
3. Install & run
pnpm install # or npm install / yarnpnpm dev
Open http://localhost:3000.
4. Try collaboration with two users
- In your main browser, pick a demo user on the sign-in page, then create a document.
- Open a second browser (or an incognito window) and sign in as a different demo user.
- That user's dashboard is empty — the document is restricted and not shared yet.
- Back in the first browser, click Share and invite the second user as Editor.
- Reload the second browser — the document now appears and opens. Type together, add comments, and watch presence avatars + live cursors update in real time.
- Change the second user to Viewer and reload — their editor becomes read-only. Revoke access and reload — they're locked out.
How it works
Authentication (demo)
app/userAuth/AppUserContext.tsx stores the demo user you pick on /signin in
localStorage. The fixed demo users live in lib/demoUsers.ts.
In your real app: delete the demo pieces and pass your authenticated user (from NextAuth, Clerk, Auth0, …) to Velt. The only integration point is
components/velt/VeltInitializeUser.tsx.
Identifying the user to Velt
components/velt/VeltInitializeUser.tsx builds the Velt authProvider. Its
generateToken callback calls POST /api/velt/token, which mints a JWT via the
Velt REST API encoding the user's access to the organization and the document
they're opening (with their viewer / editor role).
Documents, sharing & permissions
Velt is the source of truth for collaboration data and permission enforcement, but it has no "list every document a user can access" query — so the dashboard keeps a small directory of its own.
lib/veltRest.ts— server-only wrapper around the Velt REST API (create / rename / delete documents, set access type, grant / revoke permissions).lib/documentStore.ts— the per-organization document directory used to render each user's dashboard. This is the only place that touches storage.
| Action | Route | Velt REST calls |
|---|---|---|
| Create | POST /api/documents | documents/add → documents/access/update (restricted) → auth/permissions/add (owner = editor) |
| List | GET /api/documents | — (reads the directory) |
| Rename / Delete | PATCH / DELETE /api/documents/[id] | documents/update / documents/delete |
| Share / Revoke | POST / DELETE /api/documents/[id]/share | auth/permissions/add / auth/permissions/remove |
A newly created document is set to restricted, so only explicitly invited
users can access it. AccessGate (app/documents/[id]/AccessGate.tsx) mirrors
this on the client: no access → locked screen, viewer → read-only editor.
Velt also ships a drop-in
<VeltUserInviteTool />share UI. This kit uses a customShareMenuso sharing is wired to the backend and the dashboard directory, but you can swap in the native tool if you prefer.
The document directory: local dev vs. hosting
lib/documentStore.ts picks its backend automatically:
- Local development — no config needed. It writes one JSON file per org under
data/(gitignored). - Hosted / serverless (e.g. Vercel) — the runtime filesystem is read-only,
so the JSON-file path can't write (you'll see
ENOENT … mkdir '/var/task/.../data'). Provision a Vercel KV (Upstash Redis) store and the directory is kept in Redis instead.
Deploying to Vercel:
- In your Vercel project, open Storage → Create Database → KV (Upstash Redis)
and connect it to the project. Vercel injects
KV_REST_API_URLandKV_REST_API_TOKENautomatically. - Add
NEXT_PUBLIC_VELT_API_KEYandVELT_AUTH_TOKENas environment variables. - Redeploy. The store now uses Redis (durable across instances).
lib/documentStore.ts is the only place that touches storage, so swapping Redis
for Postgres/SQLite/etc. is a single-file change.
Project structure
app/signin/ demo-user pickerdashboard/ list / create / rename / delete documentsdocuments/[id]/ collaborative editor + ShareMenu + AccessGatedocument/ document metadata context (role, members)userAuth/ demo auth (swap for your real auth)api/velt/token/ mint Velt JWTs (encodes per-document role)documents/ create / list / rename / delete / sharecomponents/document/ TipTap editor (CRDT + comments wiring)velt/ Velt provider, document setup, tools, collaborationheader/ sidebar/ ui/ app chromelib/demoUsers.ts fixed demo usersveltRest.ts server-only Velt REST wrapperdocumentStore.ts document directory (swap for a DB in production)


