deiucanta
deiucanta3w ago

How to model "custom fields" in Convex?

I want to allow the user to define a set of custom fields similar to Asana or Notion Databases. What is the best way to do that in Convex?
5 Replies
Convex Bot
Convex Bot3w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
ian
ian3w ago
I would have one table that holds schemas (either a static set you define, or something like a serialized schema), and then in the user tables, have something like
userFields: v.array(v.object({ name: v.string(), value: v.any() }));
userFields: v.array(v.object({ name: v.string(), value: v.any() }));
or
userFields: v.record(v.string(), v.any())
userFields: v.record(v.string(), v.any())
deiucanta
deiucantaOP3w ago
Can I index those? Can I filter/soft by user fields? @Ian
Aboud
Aboud3w ago
// Custom field groups for organizing fields
export const customFieldGroupSchema = v.object({
owner_id: v.id('users'),
name: v.string(),
slug: v.string(),
resource_type: customFieldResourceTypeEnum,
display_order: v.number(),
metadata,
state: recordStateEnum,
})

export const customFieldGroups = defineTable(customFieldGroupSchema)
.index('byOwnerStateResourceType', ['owner_id', 'state', 'resource_type'])
//...

export type CustomFieldGroup = Infer<typeof customFieldGroupSchema>

// Custom field definitions
export const customFieldSchema = v.object({
owner_id: v.id('users'),
group_id: v.id('customFieldGroups'),
resource_type: customFieldResourceTypeEnum,
type: customFieldTypeEnum,
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
schema: v.any(), // Field-specific configuration (e.g., select options)
ui: v.any(), // UI display settings
is_required: v.boolean(),
display_order: v.number(),
metadata,
state: recordStateEnum,
})

export const customFields = defineTable(customFieldSchema)
.index('byOwnerState', ['owner_id', 'state'])
//...

export type CustomField = Infer<typeof customFieldSchema>

// Custom field values - stores actual data
export const customFieldValueSchema = v.object({
owner_id: v.id('users'),
field_id: v.id('customFields'),
resource_type: customFieldResourceTypeEnum,
resource_id: v.string(), // ID of the contact or transaction
value: v.any(), // Flexible value storage
metadata,
state: recordStateEnum,
updated_by: v.optional(v.id('users')),
created: v.number(),
updated: v.number(),
})

export const customFieldValues = defineTable(customFieldValueSchema)
.index('byOwnerResourceState', ['owner_id', 'resource_type', 'resource_id', 'state'])
.index('byOwnerResourceField', ['owner_id', 'resource_type', 'resource_id', 'field_id'])
// Custom field groups for organizing fields
export const customFieldGroupSchema = v.object({
owner_id: v.id('users'),
name: v.string(),
slug: v.string(),
resource_type: customFieldResourceTypeEnum,
display_order: v.number(),
metadata,
state: recordStateEnum,
})

export const customFieldGroups = defineTable(customFieldGroupSchema)
.index('byOwnerStateResourceType', ['owner_id', 'state', 'resource_type'])
//...

export type CustomFieldGroup = Infer<typeof customFieldGroupSchema>

// Custom field definitions
export const customFieldSchema = v.object({
owner_id: v.id('users'),
group_id: v.id('customFieldGroups'),
resource_type: customFieldResourceTypeEnum,
type: customFieldTypeEnum,
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
schema: v.any(), // Field-specific configuration (e.g., select options)
ui: v.any(), // UI display settings
is_required: v.boolean(),
display_order: v.number(),
metadata,
state: recordStateEnum,
})

export const customFields = defineTable(customFieldSchema)
.index('byOwnerState', ['owner_id', 'state'])
//...

export type CustomField = Infer<typeof customFieldSchema>

// Custom field values - stores actual data
export const customFieldValueSchema = v.object({
owner_id: v.id('users'),
field_id: v.id('customFields'),
resource_type: customFieldResourceTypeEnum,
resource_id: v.string(), // ID of the contact or transaction
value: v.any(), // Flexible value storage
metadata,
state: recordStateEnum,
updated_by: v.optional(v.id('users')),
created: v.number(),
updated: v.number(),
})

export const customFieldValues = defineTable(customFieldValueSchema)
.index('byOwnerResourceState', ['owner_id', 'resource_type', 'resource_id', 'state'])
.index('byOwnerResourceField', ['owner_id', 'resource_type', 'resource_id', 'field_id'])
i've been doing this if that helps The customFields.schema attribute is a JSON Schema (less control but serializable).

Did you find this page helpful?