We all like to work with tools like Figma, Notion or Linear. These are all convenient, collaborative and real-time user interfaces in our hands, which make it a pleasure to work with.
You might think creating such a complex apps is only possible for million-dollar companies, but with InstantDB, a little AI magic and a little time to spare, we can get impressively close on our own.
In my previous article, I already presented how we can make a real-time chart component with Supabase and Tremor charts.
First, let's see what open source system our application will be built around? This is Instant.
Let's see what Instant is!
InstantDB is a modern, Firebase-like database solution designed for real-time, collaborative applications. It allows developers to manage relational queries directly in the frontend without needing to handle backend complexities like servers, sockets, or authentication. InstantDB supports features like optimistic updates, offline mode, and real-time sync, making it ideal for building responsive, multiplayer applications that work seamlessly even when offline.
So what are we going to build? Charli XCX's Brat album and the BratGenerator web interface they created for it, inspired me to do something similar.
It will look like this:
In terms of functions, this application has been further developed to the extent that the creations can be viewed, liked and we can even track these on a leaderboard.
Let's see how we can build this application! Before we go into the description of the codebase, you can find the entire project here on my Github.
Here is the X post from the app and the Github repo:
This was even shared by one of the creators of Instant. :)
By the way, you can access the live project here, deployed on Vercel.
Before cloning, it will be worthwhile to register on the Instant platform. You can do this here.
After registration, create a new application called realtime-brat-generator
on the interface. We should see an interface like this at the top of the window:
Once we're done with that, let's clone the repo I linked above.
We can start this with the commands in the readme. Once we are done with that, the first thing to do is to rewrite the App ID to our own. This is found in the db.ts
file and it references the .env
file.
Then, after launch, we can already see that the application appears. But let's take a look at what exactly this app consists of.
If we go into package.json, we can see that the following technologies are used in the application:
- Next.js
- InstantDB
- TailwindCSS
- ShadCN
- Eslint / Prettier
We can also look at the basics of the project. First, if we look at the db.ts
file:
import { init } from '@instantdb/react';
import { Schema } from '@/lib/types';
export const APP_ID = process.env.APP_ID as string;
export const db = init<Schema>({ appId: APP_ID });
Then we can see that here we define our database connection and our schema.
Our schema is in the types.ts
file:
export type Schema = {
bratCreations: BratCreation;
votes: Vote;
};
export interface BratCreation {
id: string;
text: string;
preset: string;
createdAt: number;
createdBy: string;
}
export interface Vote {
id: string;
createdUserId: string;
createdAt: number;
bratCreationId: string;
orientation: 'upvote' | 'downvote';
}
export interface User {
id: string;
email: string;
}
Simple enough, right? This database structure can also be seen on the Instant interface. Here in the following image:
On the Explorer page, we can see the current content of our database and how it is structured.
Let's go to page.tsx
where we can see Instant in its full glory. This line of code queries the reactive data and we can use this hook as the useState() hook from React.
const { isLoading, error, data } = db.useQuery({
bratCreations: {},
votes: {},
});
Here, we practically do not have to do anything with this, it will be immediately reactive and automatically update the interface if something changes in our data.
We can also perform filtering very easily here, you can read more about it here.
Let's take a look at the Instant specific points of interest in the same component:
if (existingVote) {
if (existingVote.orientation === orientation) {
db.transact(tx.votes[existingVote.id].delete());
} else {
db.transact(tx.votes[existingVote.id].update({ orientation }));
}
} else {
db.transact(
tx.votes[id()].update({
createdUserId: user.id,
createdAt: Date.now(),
bratCreationId: creationId,
orientation,
})
);
}
};
Here you can see the code snippet where we can update and delete very easily with the Instant query language. The toggle operation of the liking system can be found here.
Another very interesting thing in the same file is authentication.
const { user } = db.useAuth();
With this line, we can query our currently logged-in user. This will be important because of the liking function. Only logged in users will be able to like.
The entry itself is managed by these lines of code:
const handleSendMagicCode = async (e: React.FormEvent) => {
e.preventDefault();
if (!authState.email) return;
setAuthState({ ...authState, sentEmail: authState.email, error: null });
try {
await db.auth.sendMagicCode({ email: authState.email });
} catch (error) {
if (error instanceof Error) {
setAuthState({ ...authState, error: error.message });
}
}
};
const handleVerifyMagicCode = async (e: React.FormEvent) => {
e.preventDefault();
if (!authState.code) return;
try {
await db.auth.signInWithMagicCode({
email: authState.sentEmail,
code: authState.code,
});
setIsAuthModalOpen(false);
setAuthState({ sentEmail: '', email: '', error: null, code: '' });
} catch (error) {
if (error instanceof Error) {
setAuthState({ ...authState, error: error.message });
}
}
};
As you can see, we use email login and registration, which we can configure very easily on the appropriate interface of Instant, here:
However, for this we need to define Permissions in the application, on the Permissions tab in Instant DB. Here is the screen for that:
And also the code:
{
"votes": {
"bind": [
"isOwner",
"auth.id == data.createdUserId"
],
"allow": {
"create": "auth.id != null",
"delete": "isOwner",
"update": "isOwner"
}
}
}
In the permissions we must only "lock" the vote creations, because only authenticated users can create votes and of course only the vote creator (owner) can modify and delete these votes.
What's even more exciting is the code in BratCreationForm.tsx
, where we can save our current Brat creation:
const handleSave = () => {
const bratCreation = {
text: bratText,
preset: selectedPreset.value,
createdAt: Date.now(),
};
db.transact(
tx.bratCreations[id()].update({
...bratCreation,
})
);
setShowSaveAnimation(true);
setShowConfetti(true);
setConfettiComplete(false);
setTimeout(() => {
setShowSaveAnimation(false);
}, 2000);
};
And with that, we practically covered all CRUD operations with Instant DB in our application.
Now let's test the application!
If you also feel like developing with InstantDB, feel free to fork this Github repo and add new features to it, make pull requests or start from scratch in a new application in a few minutes using this tutorial:
https://www.instantdb.com/docs
I hope this article was useful. If you have any questions, you can always find me here in the comment section or on X and we can talk! 🙂
I should also mention that this post was made possible thanks to ShiwaForce, where I’ve had the chance to work on projects like this and bring ideas to life.
Top comments (0)