Skip to content

Firestore Schema

Firestore is fast, flexible, and developer-friendly—but out of the box, it's not typesafe. This becomes a problem at scale. Enter arkfire, our in-house typesafe Firestore ORM. With a little TypeScript trickery, we infer types based on your Firestore paths, giving you full autocompletion and error checking without manual mapping.

Firestore Type

Firestores type is recursive, making the definition quite small.

ts
export type CollectionDef<Document = any> = {
    $doc: Document // defines the type of each document in the collection
    $collections: FirestoreSchemaBase // collections on each document
}

type FirestoreSchema = Record<string, CollectionDef>

Defining a schema

typescript
export type FirestoreSchema = {
    // Define the collection name
    users: {
        // user document type definition
        $doc: {
            id: string
        }

        // Define collections each
        // document should have
        $collections: {
            emails: {
                $doc: {
                    email: string
                }
            }

            // ... More collections
        }
    }

    // Here is a collection holding some small data documents.
    // In this case instead we just want to hold single documents as
    // a single type. We can define a type for an individual id as shown below.
    data: {
        $doc: never // definition of any id within the data collection
        $collections: {}

        // type def for "data/stats" document.
        stats: Stats
        // type def for "data/stats-data" document.
        "stats-data": StatsData // doc specific type def

        // type def for "data/counters" document.
        counters: {
            activeCustomers: number
            turnover: number
            invoices: number
            newClients: number
        }
    }

    // .. More collections
}

// TS Output Examples
const user = $doc("users/123") // { id: string }
const users = $col("users") // { id: string }[]

const usersEmails = $col("users/123/emails") // { email: string }[]
const usersEmail = $doc("users/123/emails/123") // { email: string }

Top level we define a Record<string, CollectionDef> for our collections.

Inside each collection were defining the type for all documents using the $doc selector.

$doc

This represents the type of any document pulled at random. You could represent as a discriminated union to store more than one type in the collection.

Or, for more fine grained control, specify a type per document id.

$doc[docId]

Inside of a CollectionDef you can define individual document type by using the document id as the key to a type.

ts
// data is the name of a collection : value is type `CollectionDef`
data: {
    $doc: never // here we write never because were defining all types specifically.
    $collections: {} // no collections

    // type def for "data/stats" document.
    stats: Stats
    // type def for "data/stats-data" document.
    "stats-data": StatsData // doc specific type def

    // type def for "data/counters" document.
    counters: {
        activeCustomers: number
        turnover: number
        invoices: number
        newClients: number
    }
}

const stats = $col("data/stats") // Stats
const statsData = $doc("data/stats-data") // StatsData
const counters = $doc("data/counters") // Counters
const counters = $doc("data/randomId-123-123") // never : ts error

$collections

Here were back where we started. With Record<string, CollectionDef> completing the loop. Now you can define here as many collections and their documents an so on.