Documentation Index Fetch the complete documentation index at: https://mintlify.com/rjdellecese/confect/llms.txt
Use this file to discover all available pages before exploring further.
While Confect allows you to use Effect schemas to define your database tables and function signatures, there are some restrictions due to Convex’s runtime validation requirements. This guide explains these limitations and how to work within them.
Why Restrictions Exist
Convex requires runtime validation of all data entering the database. Confect converts Effect schemas to Convex validators, but not all Effect schema features can be translated to Convex’s validator format.
These restrictions apply to database schemas and function args/returns . You can use full Effect schema features in your application logic.
Supported Schema Types
The following Effect schema types work seamlessly with Confect:
Primitives
Schema.String String values
Schema.Number Numeric values (including NaN, Infinity)
Schema.Boolean Boolean values (true/false)
Schema.BigInt BigInt values
import { Schema } from "effect" ;
const Fields = Schema . Struct ({
name: Schema . String ,
age: Schema . Number ,
active: Schema . Boolean ,
balance: Schema . BigInt ,
deletedAt: Schema . Null ,
});
Literals
const Status = Schema . Literal ( "draft" , "published" , "archived" );
const Priority = Schema . Literal ( 1 , 2 , 3 );
const Flag = Schema . Literal ( true );
Collections
// Arrays
const Tags = Schema . Array ( Schema . String );
const Scores = Schema . Array ( Schema . Number );
// Nested arrays
const Matrix = Schema . Array ( Schema . Array ( Schema . Number ));
Convex does not support Sets or Maps. Use arrays of unique values instead.
Objects
const User = Schema . Struct ({
name: Schema . String ,
email: Schema . String ,
age: Schema . Number ,
profile: Schema . Struct ({
bio: Schema . String ,
website: Schema . optionalWith ( Schema . String , { exact: true }),
}),
});
Unions
// Simple unions
const StringOrNumber = Schema . Union (
Schema . String ,
Schema . Number
);
// Tagged unions (discriminated unions)
const Content = Schema . Union (
Schema . Struct ({
type: Schema . Literal ( "text" ),
content: Schema . String ,
}),
Schema . Struct ({
type: Schema . Literal ( "image" ),
url: Schema . String ,
alt: Schema . String ,
})
);
Use tagged unions (with a discriminant field) for better type narrowing.
Optional Fields
const User = Schema . Struct ({
name: Schema . String ,
// Optional field - must use exact: true for Convex
email: Schema . optionalWith ( Schema . String , { exact: true }),
// Alternative: union with undefined
phone: Schema . Union ( Schema . String , Schema . Undefined ),
});
Always use { exact: true } with Schema.optionalWith for Convex compatibility.
Confect-Specific Types
import { GenericId , SystemFields } from "@confect/core" ;
// Document IDs
const noteId = GenericId . GenericId ( "notes" );
// Document with system fields
const NoteDoc = Schema . Struct ({
text: Schema . String ,
}). pipe ( SystemFields . withSystemFields ( "notes" ));
Restricted Schema Types
The following Effect schema features cannot be used in database schemas or function signatures:
Convex validators don’t support refinements or transformations.
// ❌ NOT SUPPORTED in database/function schemas
const Email = Schema . String . pipe (
Schema . pattern ( / ^ [ ^ @ ] + @ [ ^ @ ] + $ / )
);
const PositiveNumber = Schema . Number . pipe (
Schema . greaterThan ( 0 )
);
const TrimmedString = Schema . String . pipe (
Schema . transform ( Schema . String , {
decode : ( s ) => s . trim (),
encode : ( s ) => s ,
})
);
Workaround: Validate in your implementation:
import { FunctionImpl } from "@confect/server" ;
import { Effect , Schema } from "effect" ;
const EmailPattern = Schema . String . pipe (
Schema . pattern ( / ^ [ ^ @ ] + @ [ ^ @ ] + $ / )
);
export const create = FunctionImpl . make (
api ,
"users" ,
"create" ,
({ email }) =>
Effect . gen ( function* () {
// Validate in implementation
yield * Schema . decode ( EmailPattern )( email );
const writer = yield * DatabaseWriter ;
return yield * writer . table ( "users" ). insert ({ email });
}). pipe ( Effect . orDie ),
);
String Constraints
// ❌ NOT SUPPORTED
const MinLength = Schema . String . pipe ( Schema . minLength ( 5 ));
const MaxLength = Schema . String . pipe ( Schema . maxLength ( 100 ));
const Email = Schema . String . pipe ( Schema . pattern ( / ^ [ ^ @ ] + @ [ ^ @ ] + $ / ));
Workaround: Validate in your Effect logic:
Effect . gen ( function* () {
if ( text . length < 5 ) {
return yield * Effect . fail ( new Error ( "Text too short" ));
}
// Continue...
})
Number Constraints
// ❌ NOT SUPPORTED
const Positive = Schema . Number . pipe ( Schema . positive ());
const Range = Schema . Number . pipe (
Schema . between ( 0 , 100 )
);
const Integer = Schema . Number . pipe ( Schema . int ());
Dates
Convex doesn’t have a native Date type. Store timestamps as numbers instead.
// ❌ NOT SUPPORTED
const CreatedAt = Schema . Date ;
// ✅ USE THIS INSTEAD
const CreatedAt = Schema . Number ; // Store as milliseconds since epoch
Working with dates:
import { FunctionImpl } from "@confect/server" ;
import { Effect } from "effect" ;
export const create = FunctionImpl . make (
api ,
"notes" ,
"create" ,
({ text }) =>
Effect . gen ( function* () {
const writer = yield * DatabaseWriter ;
return yield * writer . table ( "notes" ). insert ({
text ,
createdAt: Date . now (), // Store as number
});
}). pipe ( Effect . orDie ),
);
Complex Types
// ❌ NOT SUPPORTED
const Tags = Schema . Set ( Schema . String );
const Metadata = Schema . Map ( Schema . String , Schema . Any );
const Tuple = Schema . Tuple ( Schema . String , Schema . Number );
const Records = Schema . Record ( Schema . String , Schema . Number );
Workaround for Sets:
// Use arrays with unique values
const Tags = Schema . Array ( Schema . String );
// Ensure uniqueness in implementation
const uniqueTags = [ ... new Set ( tags )];
Workaround for Records:
// ❌ Schema.Record not supported
const Metadata = Schema . Record ( Schema . String , Schema . String );
// ✅ Use array of objects instead
const Metadata = Schema . Array (
Schema . Struct ({
key: Schema . String ,
value: Schema . String ,
})
);
Recursive Schemas
Recursive schemas are not supported in Convex.
// ❌ NOT SUPPORTED
interface Category {
name : string ;
subcategories : Category [];
}
const Category : Schema . Schema < Category > = Schema . Struct ({
name: Schema . String ,
subcategories: Schema . Array ( Schema . suspend (() => Category )),
});
Workaround: Flatten the hierarchy or use a maximum depth:
// Option 1: Flatten with parent references
const Category = Schema . Struct ({
name: Schema . String ,
parentId: Schema . optionalWith ( GenericId . GenericId ( "categories" ), { exact: true }),
});
// Option 2: Fixed depth
const Category = Schema . Struct ({
name: Schema . String ,
level1: Schema . optionalWith (
Schema . Struct ({
name: Schema . String ,
level2: Schema . optionalWith (
Schema . Struct ({
name: Schema . String ,
}),
{ exact: true }
),
}),
{ exact: true }
),
});
Best Practices
Separate Validation Layers
Use simple schemas for Convex, rich schemas for validation:
// Database schema - simple
const UserFields = Schema . Struct ({
email: Schema . String ,
age: Schema . Number ,
});
// Validation schema - rich (use in implementation)
const ValidEmail = Schema . String . pipe (
Schema . pattern ( / ^ [ ^ @ ] + @ [ ^ @ ] + $ / )
);
const ValidAge = Schema . Number . pipe (
Schema . between ( 0 , 120 )
);
// Validate in implementation
export const create = FunctionImpl . make (
api ,
"users" ,
"create" ,
({ email , age }) =>
Effect . gen ( function* () {
yield * Schema . decode ( ValidEmail )( email );
yield * Schema . decode ( ValidAge )( age );
const writer = yield * DatabaseWriter ;
return yield * writer . table ( "users" ). insert ({ email , age });
}). pipe ( Effect . orDie ),
);
Use Branded Types
import { Brand } from "effect" ;
// Define branded type
type Email = string & Brand . Brand < "Email" >;
const Email = Brand . nominal < Email >();
// Validation function
const validateEmail = ( s : string ) : Effect . Effect < Email > =>
/ ^ [ ^ @ ] + @ [ ^ @ ] + $ / . test ( s )
? Effect . succeed ( Email ( s ))
: Effect . fail ( new Error ( "Invalid email" ));
// Use in implementation
export const create = FunctionImpl . make (
api ,
"users" ,
"create" ,
({ email }) =>
Effect . gen ( function* () {
const validEmail = yield * validateEmail ( email );
const writer = yield * DatabaseWriter ;
return yield * writer . table ( "users" ). insert ({ email: validEmail });
}). pipe ( Effect . orDie ),
);
Document Constraints
Add validation rules to your documentation:
/**
* User table
*
* Constraints:
* - email: must be valid email format
* - age: must be between 0 and 120
* - username: must be 3-20 characters, alphanumeric only
*/
export const Users = Table . make ({
name: "users" ,
Fields: Schema . Struct ({
email: Schema . String ,
age: Schema . Number ,
username: Schema . String ,
}),
Indexes: Table . indexes ({
by_email: [ "email" ],
}),
});
Testing Schemas
Test that your schemas work with Convex:
import { SchemaToValidator } from "@confect/server" ;
import { test , expect } from "vitest" ;
test ( "User schema converts to Convex validator" , () => {
const validator = SchemaToValidator . schemaToValidator ( UserFields );
// Test valid data
expect (() => validator ({ email: "test@example.com" , age: 25 })). not . toThrow ();
// Test invalid data
expect (() => validator ({ email: 123 , age: 25 })). toThrow ();
});
Common Patterns
Enum-like Values
Use literal unions:
const Status = Schema . Literal ( "draft" , "published" , "archived" );
const Role = Schema . Literal ( "admin" , "user" , "guest" );
Timestamps
Store as numbers:
const Event = Schema . Struct ({
name: Schema . String ,
timestamp: Schema . Number , // milliseconds since epoch
});
File References
Use storage IDs:
import { GenericId } from "@confect/core" ;
const Post = Schema . Struct ({
title: Schema . String ,
imageId: Schema . optionalWith (
GenericId . GenericId ( "_storage" ),
{ exact: true }
),
});
JSON Data
Use nested structs:
// Instead of Schema.Any or Schema.Object
const Settings = Schema . Struct ({
theme: Schema . Literal ( "light" , "dark" ),
notifications: Schema . Struct ({
email: Schema . Boolean ,
push: Schema . Boolean ,
}),
});
Summary
✅ Supported Primitives, literals, arrays, objects, unions, optional fields
❌ Not Supported Refinements, transformations, dates, sets, maps, records, recursion
Workaround Validate in implementation layer with full Effect schema features
Testing Test schema conversion with SchemaToValidator
Next Steps
Services Learn how to use services in implementations
Project Structure See how to organize schemas in your project