🎨 instldraw: Team-oriented tldraw built w/ InstantDB
Welcome! We took tldraw's infinite canvas and added real-time team collaboration powered by Instant's graph database and presence.
demo.mp4
- Auth via "magic code" login.
- A full-fledged teams/memberships/invites data model and secured by permissions.
- Multiplayer cursors via presence.
⚡ 1. Create a free account on Instant
Head on over to the Instant dashboard, grab your app's ID, and plop it into a .env.development.local
file:
NEXT_PUBLIC_INSTANT_APP_ID=__YOUR_APP_ID__
This is a Next.js project bootstrapped with create-next-app
.
Install dependencies with your package manager of choice, then start the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
That's it! 🎉 Open http://localhost:3000 with your browser to see the result. Next.js will live-update the page as you edit the app's code.
Finally, to add a layer of security to your app, copy resources/instant-perms.json
into the Instant Permissions editor.
The app is broken up into two pages:
- An index page
/
, which serves as a dashboard and directory of teams and drawings. - A drawing page
/drawings/:id
where we render the tldraw canvas.
Both pages load data from Instant with db.useQuery
and write data using functions from src/mutators.ts
.
src/pages/index.tsx
: The main dashboard: list and manage teams, teammates, and drawings.src/pages/drawings/[id].tsx
: The canvas! UsesuseInstantStore
anduseInstantPresence
to add multiplayer.src/lib/useInstantStore.tsx
: A collaborative backend for tldraw built on top of Instant's real-time database. Uses InstaML's merge() for fine-grained updates to the drawing state.src/lib/useInstantPresence.tsx
: A React hook responsible for keeping tldraw's editor state in sync with Instant's real-time presence API.src/mutators.ts
: All functions that update Instant's database live here. You can inspect and edit your database using the Instant Explorer.
drawings {
teams: @has_one(teams)
state: DrawingState{} # see src/types.ts
name: string
}
teams {
drawings: @has_many(drawings)
invites: @has_many(invites)
memberships: @has_many(memberships)
name: string
creatorId: string
}
invites {
teams: @has_one(teams)
teamId: string
teamName: string
userEmail: string
}
memberships {
teams: @has_one(teams)
teamId: string
userId: string
userEmail: string
}
Instant provides a permissions layer to define access control for all entities in your database using CEL. Copy the contents of resources/instant-perms.json
below into your app's permissions editor.
{
"teams": {
"bind": [
"isCreator",
"auth.id == data.creatorId",
"isMember",
"auth.id in data.ref('memberships.userId')"
],
"allow": {
"view": "isMember",
"create": "isCreator",
"delete": "isCreator",
"update": "isCreator"
}
},
"invites": {
"bind": [
"isMember",
"auth.id in data.ref('teams.memberships.userId')",
"isInvitee",
"auth.email == data.userEmail"
],
"allow": {
"view": "isInvitee",
"create": "isMember",
"delete": "isMember",
"update": "false"
}
},
"drawings": {
"bind": [
"isMember",
"auth.id in data.ref('teams.memberships.userId')"
],
"allow": {
"view": "isMember",
"create": "isMember",
"delete": "isMember",
"update": "isMember"
}
},
"memberships": {
"bind": [
"isMember",
"auth.id in data.ref('teams.memberships.userId')",
"isInviteeOrCreator",
"size(data.ref('teams.invites.id')) == 0 ? auth.id in data.ref('teams.creatorId') : auth.email in data.ref('teams.invites.userEmail')",
"isUser",
"auth.id == data.userId"
],
"allow": {
"view": "isMember",
"create": "isInviteeOrCreator",
"delete": "isUser",
"update": "false"
}
}
}
data
is a variable provided by Instant that references the object being operated upon.auth
is a variable provided by Instant that you can use to access the current user's ID and email.- You can leverage Instant's graph database in your rule definitions using
data.ref(<key_path>)
.ref
will traverse an entity's links and collect the values of your specified property path.- For example,
isMember
underbind
forteams
usesdata.ref('memberships.userId')
to 1) collect all linked memberships for a given team and then 2) select theuserId
property for each membership.
- You can use the
bind
param to define abstractions (isMember
,isInvitee
) that can be re-used across rules for that namespace.
Instant is a sync engine inspired by Firebase with support for relational data. To learn more, check out this essay.