Skip to content

Instantly share code, notes, and snippets.

@kirankunigiri
Last active May 8, 2025 08:33
Show Gist options
  • Save kirankunigiri/bf07a3510e8d9493af05620fb413ff5a to your computer and use it in GitHub Desktop.
Save kirankunigiri/bf07a3510e8d9493af05620fb413ff5a to your computer and use it in GitHub Desktop.
Triplit vs. InstantDB

Triplit vs. InstantDB

I’ve been noticing that the query functions for triplit are not as advanced or easy to use as in most other database libraries. I’ve made a small write up with a full code example comparison at the end with my thoughts and comparisons in this thread.


To start with an example, here is my Triplit schema that I’m using. This is for a very simple receipt splitting app. Users can have receipts, which have a list of items. Each item can be associated with specific people who ate that item. An item can have multiple people if they all shared a specific item, so there is a many to many relation with the item_people table. For simplicity, I am excluding a users table and auth/etc.

I am going to use InstantDB as a direct comparison, but also I have to note that many other libraries like Prisma, Drizzle, Remult, ElectricSQL, Convex ents, etc. have the functionality that I will show from InstantDB, but is missing from Triplit. I am coming here after trying out those, so it feels like a slight loss in developer experience (although there are huge functional gains from triplit).

import { type ClientSchema, Schema as S } from '@triplit/client';

const schema = {
    receipts: {
        schema: S.Schema({
            id: S.Id(),
            name: S.String(),
            date: S.Date(),
            tax: S.Number(),
            items: S.RelationMany('items', {
                where: [['receipt_id', '=', '$id']],
            }),
        }),
    },
    items: {
        schema: S.Schema({
            id: S.Id(),
            name: S.String(),
            quantity: S.Number(),
            price: S.Number(),
            receipt_id: S.String(),
            receipt: S.RelationById('receipts', '$receipt_id'),
            people: S.RelationMany('item_people', {
                where: [['item_id', '=', '$id']],
            }),
        }),
    },
    people: {
        schema: S.Schema({
            id: S.Id(),
            name: S.String(),
            venmoUsername: S.Optional(S.String()),
            items: S.RelationMany('item_people', {
                where: [['person_id', '=', '$id']],
            }),
        }),
    },
    item_people: {
        schema: S.Schema({
            id: S.Id(),
            item_id: S.String(),
            person_id: S.String(),
            item: S.RelationById('items', '$item_id'),
            person: S.RelationById('people', '$person_id'),
        }),
    },
} satisfies ClientSchema;

export { schema };

Querying with include

I found my first roadblock when I tried to build a query to find a receipt with a specific id, but include all of its items, and all the people for each item. This was a nested relation include, and it took me a few tries to get it right because I was getting confused while trying to nest the functions, and even claude/chatgpt kept getting it completely wrong. The nesting of rel() functions makes it hard to understand even now, what exactly it is doing, when in reality it is just trying to include items → people → person. This pain point is regarding include in general, regardless of the fact that this is for a many-to-many relationship

const receiptsQuery = triplitClient
    .query('receipts')
    .where('id', '=', receiptId)
    .include('items', rel =>
        rel('items')
            .include('people', rel => rel('people').include('person').build())
            .build(),
    );

This is in comparison to something like InstantDB, where you have an object syntax, and just including the field name will make sure to include the relation. Other libraries share this functionality (for example, in Prisma you can write “include” with nested relations just like this).

const { isLoading, error, data } = db.useQuery({
    receipts: {
        $: { where: { id: receiptId } },
        items: { people: {} },
    },
});

Accessing Query Results (for many-to-many)

Popular libraries handle many-to-many relations for you and can abstract the intermediate table away. In triplit, the relation table is still included in results. This is a rather minor problem, but wanted to include it anways.

// Triplit result - have to access relation table -> person
receipt.items[0].people[0].person.name
// InstantDB result - people refers to the actual person table, not the intermediate relation table
receipt.items[0].people[0].name

Connecting/linking records (many-to-many)

While triplit handles one-to-one and one-to-many relations well, it doesn’t work as well for many-to-many. Most libraries have a linking/connecting function to connect records for all types of relations, including many-to-many, within an insert/update function.

In triplit, a link for a many-to-many has to be established in a second insert function.

const handlePersonCreate = async (name: string, itemId: string) => {
    await triplitClient.transact(async (tx) => {
        // Create a person
        const person = await tx.insert('people', { name });
        // Create a relation between the person and the item
        await tx.insert('item_people', {
            item_id: itemId,
            person_id: person.id,
        });
    });
};

Here is the same function with InstantDB. Other libraries handle it similarly (ex: Prisma uses a “connect” field parameter) I can also pass in an array of multiple entities if I wanted to link multiple records at once.

const handlePersonCreate = (name: string, itemId: string) => {
    db.transact([
        db.tx.people[id()]!
            .update({ name })
            .link({ items: [itemId] }),
    ]);
};

Overall, these aren’t deal breakers, but I’m finding that I am spending more time on writing CRUD related code (as it also requires literally more lines of code) compared to the various libraries I tried out in the past. Most seem to use a simple object-based syntax that allows for easy nesting, rather than the function parameter based approach of triplit.

I thought this comparison was interesting to see the differences, and maybe also show where Triplit can improve. Are there any plans/interest in any of the following?

  • Moving to an object-based syntax for CRUD operations
  • Handling many-to-many automatically

Code Example - Direct Comparison

For a direct comparison, here is a side by side of the full react components using the previous examples. TriplitExample vs InstantDBExample

import { id } from '@instantdb/react';
import { useQuery } from '@triplit/react';

import db, { triplitClient } from '~client/main';

function TriplitExample() {
    const receiptId = '1'; // example id

    // Complicated query to include a receipt, its items, and the people related to each item
    // receipt -> items -> item_people relation -> person
    const receiptsQuery = triplitClient
        .query('receipts')
        .where('id', '=', receiptId)
        .include('items', rel =>
            rel('items')
                .include('people', rel => rel('people').include('person').build())
                .build(),
        );
    const { results, fetching, error } = useQuery(triplitClient, receiptsQuery);

    if (fetching) return <div>Loading...</div>;
    if (error) return <div>Error: {error?.message}</div>;
    const receipt = results?.[0];
    if (!receipt) return <div>Receipt not found</div>;

    // Function to create a person and relate them to an item
    // Requires 2 steps, instead of just a single insert with linking/connecting functionality
    const handlePersonCreate = async (name: string, itemId: string) => {
        await triplitClient.transact(async (tx) => {
            // Create a person
            const person = await tx.insert('people', { name });
            // Create a relation between the person and the item
            await tx.insert('item_people', {
                item_id: itemId,
                person_id: person.id,
            });
        });
    };

    // Note the need to nest an extra map to access the people for each item
    return (
        <div>
            {receipt.items.map((item) => {
                // Have to do another map since receipt.items is a relation array
                const peopleForItem = item.people?.map(p => p.person);
                return (
                    <div key={item.id}>
                        {/* Show item details */}
                        <p>{item.name}</p>
                        {/* Show list of people for this item */}
                        {peopleForItem.map(person => (
                            <div key={person?.id}>{person?.name}</div>
                        ))}
                    </div>
                );
            })}
        </div>
    );
}

function InstantDBExample() {
    const receiptId = '1'; // example id

    // Simple query, just include the relation name and it will be included (even for nested relations)
    const { isLoading, error, data } = db.useQuery({
        receipts: {
            $: { where: { id: receiptId } },
            items: { people: {} },
        },
    });

    if (isLoading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    const receipt = data.receipts?.[0];
    if (!receipt) return <div>Receipt not found</div>;

    // Uses a link function to create a relation within the insert query
    // In other libraries, there is similar functionality like `connect` from prisma
    const handlePersonCreate = (name: string, itemId: string) => {
        db.transact([
            db.tx.people[id()]!
                .update({ name })
                .link({ items: [itemId] }),
        ]);
    };

    console.log(receipt.items[0]?.people[0]?.name);

    // Note that the query omits the relation table, and instead directly includes the people array
    return (
        <div>
            {receipt.items.map(item => (
                <div key={item.id}>
                    {/* Show item details */}
                    <p>{item.name}</p>
                    {/* Show list of people for this item */}
                    {item.people.map(person => (
                        <div key={person?.id}>{person?.name}</div>
                    ))}
                </div>
            ))}
        </div>
    );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment