Skip to main content

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

Schema.Null

Null 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:

Refinements and Transformations

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

Build docs developers (and LLMs) love