What if useState was your backend?

Development••14 minutes read

Stepan Parunashvili

Stepan Parunashvili

Guest Author

From šŸ˜… to šŸ™Œ in milliseconds—build a full-stack emoji chat app with Expo + Instant. Real-time, offline, and no backend blues.

What if useState was your backend?

This is a guest post by Stepan Parunashvili of Instant. Instant makes you more productive by giving your frontend a real-time database. By the end of the post you’ll build a full-stack, multiplayer chat app!

...

Expo makes frontends easier to build. Write once and you get app on iOS, Android and web. Make a change and your app hot reloads, no need to wait for binaries to build. Fix a bug and you can send patches directly to your users, you’re not stuck in app store submissions. And when it’s time to push to Testflight, don’t worry about provisioning profiles or XCode environments, run npx testflight and you’re ready to go.

A development experience like that can change both how you work and what you can build. When every save means you wait a few seconds, programming feels like drudgery. Get that down to milliseconds and programming feels like flow. When introducing a bug means hours of downtime, you’re in for a stressful release process. Get that down to live patches and you ship quickly again.

Write with flow, ship quickly, get faster feedback, and now you’re building a different app in a different way. That’s the story of frontend. But what happens when you get to the backend?

The state of backends

How do you save your user’s data? Suddenly your work is full of schleps again. Let’s remember what we need to do.

Most of the time you start with a server. You spin up a database and a backend to host your endpoints (But don’t forget Redis and a good ORM). And when you set up your endpoints, return as little data as possible or you’ll send unnecessarily large payloads to your users. Remember folks may still use old clients, so every backend update has to be backwards-compatible:

And we’re only getting started. Now you need to connect your backend to your app. First you coordinate calls to different endpoints. (Since we returned as little data as possible, we often have to make multiple fetches to paint a single screen). Then you normalize those responses and stick them into stores. Then you write selectors to get just what each screens needs. Then to handle mutations. For every save, you write your change to the store, then send it to the server, and then handle rollback:

Do this and you have a fully-fledged app. But you can’t stop there. When you build mobile apps offline mode is a real concern. Time to dust off expo-sqlite and cache your data. Make sure to keep your mutations saved too, so you can send them to the server when you’re back online. Then what if you want multiplayer? Time to add stateful servers and to worry about broadcasting changes:

Oi. When lines in a diagram cross like this, you know you’re in for trouble. And to make things worse, every new feature requires the same kind of song and dance: add models, write endpoints, stores, selectors, and finally the UI.

A useState-ish backend

That’s a lot of steps to go through to save your user’s data. Does it have to be this way? In 2021, we realized that most of these schleps were actually database problems in disguise.

Maybe a database-looking solution could solve them. For example, if you had a database inside your Expo app, you wouldn't need to think about stores, selectors, endpoints, or local caches: you’d just write queries. If these queries were multiplayer by default, you wouldn't have to worry about stateful servers. And if your database supported rollback, you'd get optimistic updates for free.

Well, we tried to build a database-looking solution, and 2 years later released Instant. Instant makes you productive by giving your frontend a real-time database.

You write queries that stay in sync and work offline. Every save updates immediately, and it comes with built-in auth and permissions. How does it look? Here’s a snippet that connects to your backend, syncs data and lets you change it too:

Code
// ą¼¼ 恤 ā—•_ā—• ༽つ Real-time Chat
// ----------------------------------
// * Updates instantly
// * Multiplayer
// * Works offline
import { init, tx, id } from "@instantdb/react-native";
const db = init({
appId: process.env.EXPO_PUBLIC_INSTANT_APP_ID,
});
function Chat() {
// 1. Read
const { isLoading, error, data } = db.useQuery({
messages: {},
});
// 2. Write
const addMessage = (message) => {
db.transact(tx.messages[id()].update(message));
};
// 3. Render!
return <UI data={data} onAdd={addMessage} />;
}

The only missing piece is in this code snippet is the UI component, everything else comes as you see it.

If you squint, useQuery and db.transact start to resemble a useState hook. For example, if you just wanted to save messages with useState, you’d write:

Code
function Chat() {
const [messages, setMessages] = useState([]);
// ...
}

Well, replace messages with db.useQuery, and setMessages with db.transact, and you have our code snippet:

Code
function Chat() {
// 1. Read
const { isLoading, error, data } = db.useQuery({
messages: {},
});
// 2. Write
const addMessage = (message) => {
db.transact(tx.messages[id()].update(message));
};
// ...
}

A few extra lines but now your data is persisted, works offline, and is multiplayer! Pretty cool.

Operation…EmojiChat

Is it really so simple though? Well, let’s put Expo and Instant to a bit of a test right inside this essay, and build an app. I suggest…EmojiChat!

Expo and Instant - EmojiChat app

Here’s the idea. WhatsApp is great, but why the need for a text box? Sometimes, all you need to communicate is a few emojis. That’s where Emoji Chat comes in.

You can use EmojiChat to express your deepest thoughts with a few well-curated emojis: ā€œšŸ”„ā€, ā€œā¤ļøā€, ā€œšŸ‘»ā€, ā€œšŸ™Œā€.

Instead of writing ā€œI miss you, want to meet up?ā€, you could simply send ā€œšŸ‘»ā¤ļøšŸ”„šŸ™Œā€. Now that’s expression!

A plan of attack

Okay, let’s build EmojiChat. Here’s what we’ll do: we’ll start with a basic Expo template and hack our app out from there. We’ll take it step by step:

  • We’ll scaffold our UI.
  • We’ll create our Instant backend.
  • We’ll add our messages table.
  • Then, we’ll connect our UI to our backend

Once that’s done, we’ll have real-time chat! Users will be able to send emojis and it will show up everywhere. We won’t stop there though. We can’t forget about permissions:

  • In the end we’ll add permissions to lock down who can change what.

Once we do that we can celebrate a bit and talk about what else we can build. Okay, let’s get hacking.

Goal 1: Let’s build our UI

First things first, time for some fun with Expo : )

create-expo

We can use our trusty npx create-expo-app and pick a starter. I really like the with-tailwindcss template:

Terminal
-Ā npx create-expo-app -e with-tailwindcss emoji-chat
-Ā cd emoji-chat
# šŸ”„ and we're off to the races
-Ā npm run start

Now we have a repo with expo-router and Nativewind. We can replace src/app/index.tsx with something we’ll start to evolve:

src/app/index.tsx
import React from "react";
import { Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
export default function Page() {
return (
<SafeAreaView className="flex flex-1 items-center justify-center">
<Text className="text-5xl text-blue-500">Hello, world!</Text>
</SafeAreaView>
);
}

Take a look at your Expo Go, and you’ll see a hello world!

Okay, next step is the core of our UI: our button.

A slick button

Normally tutorials only show you a few minimal components. But for EmojiChat we need to go deeper — at least for our Emoji Button. Our button is the primary medium for our users to express themselves, so it should feel slick. Here’s my suggestion:

This button is slick

When we press our button, it’s going to feel like it sinks inside the screen.

EmojiPill

To start, we’ll create a little EmojiPill component. This is our rounded pill that we can place emojis in. Here’s src/components/emoji.tsx:

src/components/emoji.tsx
import { Text, View } from "react-native";
export const EMOJIS = ["šŸ”„", "ā¤ļø", "šŸ‘»", "šŸ™Œ"] as const;
// This infers `type Emoji = "šŸ”„" | "ā¤ļø" | "šŸ‘»" | "šŸ™Œ"`
export type Emoji = (typeof EMOJIS)[number];
const EMOJI_SIZE = 54;
export function EmojiPill({ label }: { label: Emoji }) {
return (
<View
className="bg-gray-200 rounded-xl flex items-center justify-center"
style={{
width: EMOJI_SIZE,
height: EMOJI_SIZE,
}}
>
<Text className="text-2xl">{label}</Text>
</View>
);
}

Now to use it in our index.tsx file:

src/app/index.tsx
import React from "react";
import { EmojiPill } from "@/components/emoji";
import { SafeAreaView } from "react-native-safe-area-context";
export default function Page() {
return (
<SafeAreaView className="flex flex-1 items-center justify-center">
<EmojiPill label="šŸ‘»" />
</SafeAreaView>
);
}

And we’ve got a cool pill!

Time to turn this pill into a button.

ScaleButton

React Native’s animated library is going to come in real handy for us. Let’s head on over to src/components/ScaleButton.tsx and sketch it out:

src/components/ScaleButton.tsx
import React, { useRef } from "react";
import { Animated, Pressable, PressableProps } from "react-native";
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function ScaleButton(props: PressableProps) {
const scale = useRef(new Animated.Value(1)).current;
return (
<AnimatedPressable
onPressIn={() => {
Animated.spring(scale, {
toValue: 0.9,
useNativeDriver: true,
}).start();
}}
onPressOut={() => {
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
}).start();
}}
style={{
transform: [{ scale }],
}}
{...props}
/>
);
}

What have we done here? We’ve created a scale animated value. By default, the value is 1 , which means our button is going to be the same size. As we press in, we change the scale to 0.9, causing our button to get smaller and feel like it ā€œsinkedā€! As we press out, the button goes back to 1 (normal) again.

Let’s try it out:

src/app/index.tsx
import React from "react";
import { EmojiPill } from "@/components/emoji";
import { SafeAreaView } from "react-native-safe-area-context";
import { ScaleButton } from "@/components/ScaleButton";
export default function Page() {
return (
<SafeAreaView className="flex flex-1 items-center justify-center">
<ScaleButton>
<EmojiPill label="šŸ‘»" />
</ScaleButton>
</SafeAreaView>
);
}

And now we have our sinking button!

Emoji button demo

The layout

Now that we have our button, it’s time to build out a screen! Here’s what we want to do:

We’ll have a little bar at the bottom with our emoji buttons. Then we’ll have a scrolled container of all our messages. The tricky part in our UI is in the alignment: our emojis should line up both horizontally and vertically. For example, the ā€œghostā€ emoji should stay in the ā€œghostā€ row, the ā€œheartā€ emoji in in the ā€œheartā€ row, and so on.

The simplest way to get this done is use fixed widths.

emojiOffset

If we know the size of our emoji pills and the gaps between them, we can figure out where to place a particular emoji. Here’s emojiOffset to do just that:

src/components/emoji.tsx
// ...
export const EMOJIS = ["šŸ”„", "ā¤ļø", "šŸ‘»", "šŸ™Œ"] as const;
export type Emoji = (typeof EMOJIS)[number];
export const EMOJI_SIZE = 54;
export const EMOJI_GAP = 16;
export function emojiOffset(emoji: Emoji) {
const idx = EMOJIS.indexOf(emoji);
return idx * EMOJI_SIZE + idx * EMOJI_GAP;
}
// ...

To put it into action, this is how emojiOffset would figure out the placement for ā€œšŸ™Œā€:

Pretttty cool!

EmojiBar

Our phones can get large and FlexBox could stretch out the gaps for our emoji buttons. To make sure we place our emojis correctly, we can write up an EmojiBar, a fixed-width wrapper around our emojis:

src/components/emoji.tsx
// ...
export const BAR_WIDTH =
EMOJIS.length * EMOJI_SIZE + (EMOJIS.length - 1) * EMOJI_GAP;
export function EmojiBar({ children }: React.PropsWithChildren) {
return (
<View className="flex-row justify-center">
<View
className="flex-row"
style={{
width: BAR_WIDTH,
}}
>
{children}
</View>
</View>
);
}
// ...

Put it all together, and our emoji.tsx file looks like this:

src/components/emoji.tsx
import React from "react";
import { View, Text } from "react-native";
export const EMOJIS = ["šŸ”„", "ā¤ļø", "šŸ‘»", "šŸ™Œ"] as const;
export type Emoji = (typeof EMOJIS)[number];
export const EMOJI_SIZE = 54;
export const EMOJI_GAP = 16;
export function emojiOffset(emoji: Emoji) {
const idx = EMOJIS.indexOf(emoji);
return idx * EMOJI_SIZE + idx * EMOJI_GAP;
}
export const BAR_WIDTH =
EMOJIS.length * EMOJI_SIZE + (EMOJIS.length - 1) * EMOJI_GAP;
export function EmojiBar({ children }: React.PropsWithChildren) {
return (
<View className="flex-row justify-center">
<View
className="flex-row"
style={{
width: BAR_WIDTH,
}}
>
{children}
</View>
</View>
);
}
export function EmojiPill({ label }: { label: Emoji }) {
return (
<View
className="bg-gray-200 rounded-xl flex items-center justify-center"
style={{
width: EMOJI_SIZE,
height: EMOJI_SIZE,
}}
>
<Text className="text-2xl">{label}</Text>
</View>
);
}

Building the screen

Okay, we have all the components we need for our screen. Let’s use them. Here it goes:

src/app/index.tsx
import {
EMOJI_GAP,
EmojiBar,
EmojiPill,
EMOJIS,
Emoji,
emojiOffset,
} from "@/components/emoji";
import React, { useState } from "react";
import { SafeAreaView, ScrollView, View, Text } from "react-native";
import { ScaleButton } from "../components/ScaleButton";
type Message = {
id: string;
body: Emoji;
createdAt: number;
};
export default function Page() {
const [messages, setMessages] = useState<Message[]>([]);
return (
<SafeAreaView className="flex-1 m-4">
<ScrollView className="py-4">
{messages.toReversed().map((msg) => {
const marginLeft = emojiOffset(msg.body);
return (
<View className="mb-4" key={msg.id}>
<EmojiBar>
<View style={{ marginLeft }}>
<EmojiPill label={msg.body} />
</View>
</EmojiBar>
</View>
);
})}
</ScrollView>
<EmojiBar>
{EMOJIS.map((emoji, idx) => (
<ScaleButton
key={emoji}
onPress={() => {
setMessages((prev) => {
return [
...prev,
{
id: prev.length.toString(),
body: emoji,
createdAt: Date.now(),
},
];
});
}}
>
<View style={{ marginLeft: idx == 0 ? 0 : EMOJI_GAP }}>
<EmojiPill label={emoji} />
</View>
</ScaleButton>
))}
</EmojiBar>
</SafeAreaView>
);
}

Load our page, and we have our UI!

We made our UI!

Nice! Let’s look at what we just did.

We added a Message type:

src/app/index.tsx
// ...
type Message = {
id: string;
body: Emoji;
createdAt: number;
};
// ...

Then we created a useState hook:

src/app/index.tsx
// ...
export default function Page() {
const [messages, setMessages] = useState<Message[]>([]);
// ...
}
// ...

We render our messages into a ScrollView, and use emojiOffset to position each emoji:

src/app/index.tsx
// ...
export default function Page() {
// ...
return (
// ...
<ScrollView className="py-4">
{messages.toReversed().map((msg) => {
const marginLeft = emojiOffset(msg.body);
return (
<View className="mb-4" key={msg.id}>
<EmojiBar>
<View style={{ marginLeft }}>
<EmojiPill label={msg.body} />
</View>
</EmojiBar>
</View>
);
})}
</ScrollView>
// ...
)
}

Then we built out our buttons. Every time we press an emoji, we setMessages and add a new message:

src/app/index.tsx
// ...
export default function Page() {
// ...
return (
// ...
<EmojiBar>
{EMOJIS.map((emoji, idx) => (
<ScaleButton
key={emoji}
onPress={() => {
setMessages((prev) => {
return [
...prev,
{
id: prev.length.toString(),
body: emoji,
createdAt: Date.now(),
},
];
});
}}
>
<View style={{ marginLeft: idx == 0 ? 0 : EMOJI_GAP }}>
<EmojiPill label={emoji} />
</View>
</ScaleButton>
))}
</EmojiBar>
// ...
)
}

With that, we have our screen!

Goal 2: Create a backend

Okay, now to create our backend. At Instant we call each backend an ā€œappā€. We can create an app right inside our terminal with npx instant-cli.

login

To start we’ll log into Instant:

Terminal
-Ā npx instant-cli login
Terminal
-Ā Let's log you in!
? This will open instantdb.com in your browser, OK to proceed? yes Waiting for authentication...Successfully logged in as stopa@instantdb.com!

init

Now that you’re logged in, you can create an app:

Terminal
-Ā npx instant-cli init
Terminal
Checking for an Instant SDK...Couldn't find an Instant SDK in your package.json, let's install one!? Which package would you like to use? @instantdb/react-nativeāœ” Installed @instantdb/react-native using npm.? What would you like to do? Create a new app? What would you like to call it? emoji-chatLooks like you don't have a `.env` file yet.If we set `EXPO_PUBLIC_INSTANT_APP_ID`, we can remember the app that you chose for all future commands.? Want us to create this env file for you? yesCreated `.env` file!Pulling schema...āœ… Wrote schema to instant.schema.tsPulling perms...āœ… Wrote permissions to instant.perms.ts

Woohoo. This just:

  1. Installed @instantdb/react-native,
  2. Created a schema and a perms file.
  3. Added EXPO_PUBLIC_INSTANT_APP_ID to your env with your band new app!

Prettyy cool. If you look at your Instant Dashboard, you’ll see your app there šŸ™‚

Goal 3: Add the messages table

Let’s take a peak at the instant.schema.ts file that instant-cli created:

instant.schema.ts
import { i } from "@instantdb/react-native";
const schema = i.schema({
entities: {
$files: i.entity({
path: i.string().unique(),
url: i.any().optional(),
}),
$users: i.entity({
email: i.string().unique(),
}),
},
links: {},
rooms: {},
});
type AppSchema = typeof schema;
export type { AppSchema };
export default schema;

Aptly titled, you can manage your app’s schema in instant.schema.ts.

You’ll notice $users and $files. These are built-in entities in Instant: when a user signs up we’ll create a row in $users. When you upload a file we’ll create a row in $files. So what should happen when someone wants to send an emoji?

The messages entity

Well, let’s create a row in messages. To do that, we can need to add a messages entity to the schema:

instant.schema.ts
import { i } from "@instantdb/react-native";
import { Emoji } from "@/components/emoji";
const schema = i.schema({
entities: {
// ...
messages: i.entity({
body: i.string<Emoji>(),
createdAt: i.date(),
}),
},
links: {},
rooms: {},
});
// ..
export default schema;

This looks good. Now run push, and see your backend update!

Terminal
-Ā npx instant-cli push
Code
Checking for an Instant SDK...
Found @instantdb/react-native in your package.json.
Found EXPO_PUBLIC_INSTANT_APP_ID: d3aecdf7-e27f-4fc7-85ef-c7c772f85f1e
Planning schema...
The following changes will be applied to your production schema:
ADD ENTITY messages.id
ADD ATTR messages.body :: unique=false, indexed=false
ADD ATTR messages.createdAt :: unique=false, indexed=false
? OK to proceed? yes
Schema updated!
Planning perms...
No perms changes detected. Skipping.

If you check the Explorer in your Instant Dashboard, you’ll see messages!

Goal 4: Connect your app

Okay, we have a backend, now let’s connect our frontend! Here’s what we need to do.

db.ts

Head on over and create a src/db.ts file. This is where we’ll connect to Instant:

src/db.ts
import { init } from "@instantdb/react-native";
import schema from "instant.schema";
const db = init({
appId: process.env.EXPO_PUBLIC_INSTANT_APP_ID,
schema: schema,
});
export default db;

Aand that’s it. We can use db to make queries and transactions. When we do, our data will persist, queries will work offline, and everything will be multiplayer!

Update index.ts

Let’s put db to the test. Go ahead and update src/app/index.tsx. First, we’ll import our db and Instant’s handy id function:

src/app/index.ts
import db from "@/db";
import { id } from "@instantdb/react-native";

We can replace our useState with a query for messages:

src/app/index.ts
export default function Page() {
- const [messages, setMessages] = useState<Message[]>([]);
+ const { data } = db.useQuery({ messages: {} });
+ const messages = data?.messages || [];

And we can use db.transact to create messages:

src/app/index.tsx
- setMessages((prev) => {
- return [
- ...prev,
- {
- id: prev.length.toString(),
- body: emoji,
- createdAt: Date.now(),
- },
- ];
- });
+ db.transact(
+ db.tx.messages[id()].update({
+ body: emoji,
+ createdAt: Date.now(),
+ })
+ );

Put it all together, and here’s how src/app/index.tsx looks:

src/app/index.tsx
import {
EMOJI_GAP,
EmojiBar,
EmojiPill,
EMOJIS,
emojiOffset,
} from "@/components/emoji";
import { SafeAreaView, ScrollView, View } from "react-native";
import { ScaleButton } from "../components/ScaleButton";
import db from "@/db";
import { id } from "@instantdb/react-native";
export default function Page() {
const { data } = db.useQuery({ messages: {} });
const messages = data?.messages || [];
return (
<SafeAreaView className="flex-1 m-4">
<ScrollView className="py-4">
{messages.toReversed().map((msg) => {
const marginLeft = emojiOffset(msg.body);
return (
<View className="mb-4" key={msg.id}>
<EmojiBar>
<View style={{ marginLeft }}>
<EmojiPill label={msg.body} />
</View>
</EmojiBar>
</View>
);
})}
</ScrollView>
<EmojiBar>
{EMOJIS.map((emoji, idx) => (
<ScaleButton
key={emoji}
onPress={() => {
db.transact(
db.tx.messages[id()].update({
body: emoji,
createdAt: Date.now(),
})
);
}}
>
<View style={{ marginLeft: idx == 0 ? 0 : EMOJI_GAP }}>
<EmojiPill label={emoji} />
</View>
</ScaleButton>
))}
</EmojiBar>
</SafeAreaView>
);
}

The moment of truth…

Load Expo Go, and you have a multiplayer EmojiChat! If you look at your Explorer, you’ll see emojis update in real-time too:

the moment of truth

Wow. Let’s pause for a moment.

You now have an app with a full backend!

As we’d say in EmojiChat, šŸ‘»šŸ‘»šŸ”„šŸ™ŒšŸ™Œ

Goal 5: Locking things down with permissions

You may think to yourself, we’ve saved data, but what about permissions? What if a hacker sniffs network traffic and saves a ā€œšŸ’©ā€ emoji? Or what if they try to delete all messages?

This is where permissions come in. Head on over and take a look at instant.perms.ts:

instant.perms.ts
// Docs: <https://www.instantdb.com/docs/permissions>
import type { InstantRules } from "@instantdb/react-native";
const rules = {
/**
* Welcome to Instant's permission system!
* Right now your rules are empty. To start filling them in, check out the docs:
* <https://www.instantdb.com/docs/permissions>
*
* Here's an example to give you a feel:
* posts: {
* allow: {
* view: "true",
* create: "isOwner",
* update: "isOwner",
* delete: "isOwner",
* },
* bind: ["isOwner", "auth.id != null && auth.id == data.ownerId"],
* },
*/
} satisfies InstantRules;
export default rules;

Instant comes with permissions baked in. You can define permission rules that run for every object returned in a query or changed in a transaction.

Disable deletes

For example, to disable deletes, we could write:

instant.perms.ts
// Docs: <https://www.instantdb.com/docs/permissions>
import type { InstantRules } from "@instantdb/react-native";
const rules = {
messages: {
allow: {
delete: "false",
},
},
} satisfies InstantRules;
export default rules;

This defaults the delete rule for messages to false. Once we push it:

Terminal
-Ā npx instant-cli push
Code
Checking for an Instant SDK...
Found @instantdb/react-native in your package.json.
Found EXPO_PUBLIC_INSTANT_APP_ID: d3aecdf7-e27f-4fc7-85ef-c7c772f85f1e
Planning schema...
No schema changes detected. Skipping.
Planning perms...
The following changes will be applied to your perms:
{
+ messages: {
+ allow: {
+ delete: "false"
+ }
+ }
}
? OK to proceed? yes
Permissions updated!

If any nefarious hacker tries to delete an object, they’re out luck!

Locking down emojis

Let’s get to the šŸ’© problem. What if someone tries to hack the system and send us a ā€œšŸ’©ā€? We can add a rule for that too:

instant.perms.ts
import type { InstantRules } from "@instantdb/react-native";
const rules = {
messages: {
allow: {
delete: "false",
update: "false",
create: 'data.body in ["šŸ”„", "ā¤ļø", "šŸ‘»", "šŸ™Œ"]',
},
},
} satisfies InstantRules;
export default rules;

This makes sure that you can only create a message with a valid emoji, and you can’t change an emoji after you create it. If we push it:

Code
Checking for an Instant SDK...
Found @instantdb/react-native in your package.json.
Found EXPO_PUBLIC_INSTANT_APP_ID: d3aecdf7-e27f-4fc7-85ef-c7c772f85f1e
Planning schema...
No schema changes detected. Skipping.
Planning perms...
The following changes will be applied to your perms:
{
messages: {
allow: {
+ create: "data.body in [\\"šŸ”„\\", \\"ā¤ļø\\", \\"šŸ‘»\\", \\"šŸ™Œ\\"]"
+ update: "false"
}
}
}
? OK to proceed? yes
Permissions updated!

Locking down everything

We’ve anticipated a few things a hacker could change, but what about stuff we haven’t thought about? To be on the safer side, we can lock down the database completely, so everything that we haven’t been explicit about gets denied:

instant.perms.ts
import type { InstantRules } from "@instantdb/react-native";
const rules = {
$default: {
allow: {
$default: "false",
},
},
messages: {
allow: {
view: "true",
delete: "false",
update: "false",
create: 'data.body in ["šŸ”„", "ā¤ļø", "šŸ‘»", "šŸ™Œ"]',
},
},
} satisfies InstantRules;
export default rules;

The $default rule says, if there’s some object that we haven’t made a rule for, we’ll make sure it’s false. There’s so much more you can do, just check out the permissions docs.

Fin

Heck yeah! We’ve just created a chat app for iOS, Android, and web. It works offline, is multiplayer by default and comes with permissions. This is what happens when you use tools that help you get into flow and blast through schleps.

What’s next?

You now have a pretty cool app, but there’s more we could do!

Sometimes emojis don’t quite cut it. What if we let you express yourself with your thumbs? We could add finger kisses:

Kiss demo

And right now we just have one giant arena for all conversations. What if we added Auth and channels? You can start thinking about how to do this by reading the Instant docs (one, two), but we hope to continue the tutorial another day too šŸ™‚.

InstantDB
real-time apps
offline apps
multiplayer apps
useState backend
full-stack Expo apps

Dive in, and create your first Expo project

Learn more