Defining a Schema
Example: TypeScript and Schema
A schema is a description of
- the tables in your Convex project
- the type of documents within your tables
While it is possible to use Convex without defining a schema, adding a
schema.ts
file will give you additional type safety throughout your app.
We recommend beginning your project without a schema for rapid prototyping and then adding a schema once you've solidified your plan. To learn more see our Schema Philosophy.
Currently schemas are a "types only" feature. Adding a schema will give you precise, code generated types, but won't change the runtime behavior of your application.
In the future Convex will support enforcing your schema and rejecting documents that don't match it.
Writing schemas
Schemas are defined in a schema.ts
file in your convex/
directory and look
like:
import { defineSchema, defineTable, s } from "convex/schema";
export default defineSchema({
messages: defineTable({
body: s.string(),
user: s.id("users"),
}),
users: defineTable({
name: s.string(),
tokenIdentifier: s.string(),
}).index("by_token", ["tokenIdentifier"]),
});
This schema (which is based on our
users and auth example),
has 3 tables: channels, messages, and users. Each table is defined using the
defineTable
function. Within each table,
the document type is defined using our schema builder,
s
. In addition to the fields listed, Convex will also
automatically add _id
and _creationTime
fields. To learn more, see
System Fields.
While writing your schema, it can be helpful to consult the Convex Dashboard. The "Generate Schema" button in the "Data" view suggests a schema declaration based on the data in your tables.
Supported types
The schema builder supports all of the Convex types. It also supports union types, literal types, any types, and optional types.
Unions
You can describe fields that could be one of multiple types using s.union
:
defineTable({
stringOrNumber: s.union(s.string(), s.number()),
});
If your table stores multiple different types of documents, you can also use
s.union
at the top level:
defineTable(
s.union(
s.object({
string: s.string(),
}),
s.object({
number: s.number(),
})
)
);
Literals
Fields that are a constant can be expressed with s.literal
:
defineTable({
oneTwoOrThree: s.union(
s.literal("one"),
s.literal("two"),
s.literal("three")
),
});
Any
Fields that could take on any value can be represented with s.any()
:
defineTable({
anyValue: s.any(),
});
This corresponds to the any
type in TypeScript.
Optional fields
You can describe optional fields by wrapping their type with s.optional(...)
:
defineTable({
optionalString: s.optional(s.string()),
optionalNumber: s.optional(s.number()),
});
This corresponds to marking fields as optional with ?
in TypeScript.
Schema strictness
By default schemas are considered strict. This means that the TypeScript type produced the schema will only support tables and properties explicitly listed in your schema.
Sometimes it's useful to only define part of your schema. For example, if you
are rapidly prototyping, it could be useful to try out a new table before adding
it your schema.ts
file.
You can disable strict mode by passing strict: false
in the
DefineSchemaOptions
object:
defineSchema(
{
// Define tables here.
},
{
strict: false,
}
);
When strict mode is disabled, the schema will permit:
- Using additional tables not explicitly listed in the schema.
- Using additional properties not explicitly listed in the tables.
Using schemas
Once you've defined a schema,
npx convex dev
will produce
new versions of dataModel.d.ts
and
server.d.ts
.
Doc<TableName>
The Doc
type from
dataModel.d.ts
provides document types for all of
your tables. You can use these both when writing Convex functions and in your
React components:
import { Doc } from "../convex/_generated/dataModel";
function MessageView(props: { message: Doc<"messages"> }) {
...
}
query
and mutation
The query
and
mutation
functions in
server.js
have the same API as before but now provide
a db
with more precise types. Functions like
db.insert(table, document)
now
understand your schema. Additionally
database queries will now return the correct
document type (not any
).
Infer<typeof schemaValue>
The Infer
type allows you to turn pieces of your
schema into TypeScript types. This can be useful to remove duplication between
your schema and TypeScript types:
import { defineSchema, defineTable, Infer, s } from "convex/schema";
const nestedObject = s.object({
property: s.string(),
});
// Resolves to `{property: string}`.
export type NestedObject = Infer<typeof nestedObject>;
export default defineSchema({
tableName: defineTable({
nested: nestedObject,
}),
});