Field Types API Reference
Complete API reference for all built-in field types in OpenSaaS Stack. For usage examples and guides, see the Field Types guide.
Core Field Types
text()
String field with validation and indexing options.
import { text } from '@opensaas/stack-core/fields'
text(options?: {
validation?: {
isRequired?: boolean
length?: { min?: number; max?: number }
}
isIndexed?: boolean | 'unique'
ui?: {
displayMode?: 'input' | 'textarea'
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<string, string>
defaultValue?: string
})
Options
validation
Validation rules for the text field.
Type: object
Properties:
isRequired?: boolean- Field is required on createlength?: object- Length constraintsmin?: number- Minimum character length (default: 1 when required)max?: number- Maximum character length
Example:
title: text({
validation: {
isRequired: true,
length: { min: 3, max: 100 },
},
})
isIndexed
Database index configuration.
Type: boolean | 'unique'
Values:
true- Create non-unique index for faster queries'unique'- Create unique index (enforces uniqueness)falseor omitted - No index
Example:
email: text({
isIndexed: 'unique',
validation: { isRequired: true },
})
ui.displayMode
UI display mode for the field.
Type: 'input' | 'textarea' Default: 'input'
Example:
description: text({
ui: { displayMode: 'textarea' },
})
Database Type
Prisma: String
TypeScript Type
string (optional if not required)
integer()
Numeric field for whole numbers.
import { integer } from '@opensaas/stack-core/fields'
integer(options?: {
validation?: {
isRequired?: boolean
min?: number
max?: number
}
ui?: {
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<number, number>
defaultValue?: number
})
Options
validation
Validation rules for the integer field.
Type: object
Properties:
isRequired?: boolean- Field is required on createmin?: number- Minimum value (inclusive)max?: number- Maximum value (inclusive)
Example:
age: integer({
validation: {
isRequired: true,
min: 0,
max: 150,
},
})
defaultValue
Default value when creating new items.
Type: number
Example:
score: integer({ defaultValue: 0 })
Database Type
Prisma: Int
TypeScript Type
number (optional if not required)
checkbox()
Boolean field for true/false values.
import { checkbox } from '@opensaas/stack-core/fields'
checkbox(options?: {
defaultValue?: boolean
ui?: {
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<boolean, boolean>
})
Options
defaultValue
Default boolean value.
Type: boolean
Example:
isPublished: checkbox({ defaultValue: false })
emailVerified: checkbox({ defaultValue: true })
Database Type
Prisma: Boolean
TypeScript Type
boolean (optional if no default value)
timestamp()
Date/time field with automatic timestamp support.
import { timestamp } from '@opensaas/stack-core/fields'
timestamp(options?: {
defaultValue?: { kind: 'now' } | Date
ui?: {
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<Date, Date>
})
Options
defaultValue
Default timestamp value.
Type: { kind: 'now' } | Date
Values:
{ kind: 'now' }- Automatically set to current time on createDate- Specific date/time value
Example:
createdAt: timestamp({
defaultValue: { kind: 'now' },
})
publishedAt: timestamp()
Database Type
Prisma: DateTime
TypeScript Type
Date (optional if no default value)
Validation
Accepts:
Dateobjects- ISO 8601 datetime strings
password()
String field with automatic bcrypt hashing and secure handling.
import { password } from '@opensaas/stack-core/fields'
password(options?: {
validation?: {
isRequired?: boolean
}
ui?: {
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<string, HashedPassword>
})
Security Features
- Automatic Hashing: Plaintext passwords are automatically hashed using bcrypt with cost factor 10
- Idempotent: Already-hashed passwords are not re-hashed
- Secure Output: Query results return
HashedPasswordinstances with acompare()method - No Exposure: Only sends
{ isSet: boolean }to client (not the hash)
Options
validation
Validation rules for the password field.
Type: object
Properties:
isRequired?: boolean- Field is required on create
Example:
password: password({
validation: { isRequired: true },
})
Database Type
Prisma: String
TypeScript Type
string for input, HashedPassword for output
Usage Example
// Creating a user - password is automatically hashed
const user = await context.db.user.create({
data: {
email: 'user@example.com',
password: 'plaintextPassword', // Hashed before storage
},
})
// Authenticating - use the compare() method
const user = await context.db.user.findUnique({
where: { email: 'user@example.com' },
})
if (user && (await user.password.compare('plaintextPassword'))) {
// Password is correct
}
HashedPassword API
class HashedPassword extends String {
/**
* Compare plaintext password with hashed password
* @param plaintext - The plaintext password to verify
* @returns Promise resolving to true if password matches
*/
compare(plaintext: string): Promise<boolean>
}
Important:
- Never compare password strings directly
- Always use
await password.compare(input)for verification - Empty strings and undefined values skip hashing (allows partial updates)
select()
Enum-like field with predefined options.
import { select } from '@opensaas/stack-core/fields'
select(options: {
options: Array<{ label: string; value: string }>
validation?: {
isRequired?: boolean
}
defaultValue?: string
ui?: {
displayMode?: 'select' | 'segmented-control' | 'radio'
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<string, string>
})
Options
options (required)
Array of available options.
Type: Array<{ label: string; value: string }>
Properties:
label- Display text shown to usersvalue- Actual value stored in database
Example:
status: select({
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Archived', value: 'archived' },
],
})
defaultValue
Default selected value (must match one of the option values).
Type: string
Example:
status: select({
options: [
/* ... */
],
defaultValue: 'draft',
})
validation.isRequired
Whether the field is required.
Type: boolean
ui.displayMode
UI component to use for selection.
Type: 'select' | 'segmented-control' | 'radio' Default: 'select'
Values:
'select'- Dropdown select menu'segmented-control'- Button group (good for 2-4 options)'radio'- Radio button group
Database Type
Prisma: String
TypeScript Type
Union of option values (e.g., 'draft' | 'published' | 'archived')
relationship()
Foreign key relationship to another list.
import { relationship } from '@opensaas/stack-core/fields'
relationship(options: {
ref: string
many?: boolean
ui?: {
displayMode?: 'select' | 'cards'
[key: string]: unknown
}
access?: FieldAccess
})
Options
ref (required)
Reference to related list in format 'ListName.fieldName'.
Type: string
Format: 'ListName.fieldName' where:
ListName- The target list (PascalCase)fieldName- The field on the target list that references back
Example:
// User.posts -> Post.author relationship
User: list({
fields: {
posts: relationship({
ref: 'Post.author',
many: true,
}),
},
})
Post: list({
fields: {
author: relationship({
ref: 'User.posts',
}),
},
})
many
Whether this is a one-to-many relationship.
Type: boolean Default: false
Values:
true- One-to-many (e.g., User has many Posts)false- Many-to-one or one-to-one (e.g., Post has one Author)
ui.displayMode
UI component for selecting related items.
Type: 'select' | 'cards' Default: 'select'
Values:
'select'- Dropdown select for choosing related items'cards'- Card-based UI for managing relationships
Relationship Patterns
One-to-Many
User: list({
fields: {
posts: relationship({ ref: 'Post.author', many: true }),
},
})
Post: list({
fields: {
author: relationship({ ref: 'User.posts' }),
},
})
Many-to-One
Post: list({
fields: {
author: relationship({ ref: 'User.posts' }),
},
})
User: list({
fields: {
posts: relationship({ ref: 'Post.author', many: true }),
},
})
One-to-One
User: list({
fields: {
profile: relationship({ ref: 'Profile.user' }),
},
})
Profile: list({
fields: {
user: relationship({ ref: 'User.profile' }),
},
})
Database Type
Prisma: Foreign key relationship with @relation directive
TypeScript Type
many: false-string(ID of related item, optional)many: true-string[](array of IDs, optional)
json()
Field for storing arbitrary JSON data.
import { json } from '@opensaas/stack-core/fields'
json(options?: {
validation?: {
isRequired?: boolean
}
ui?: {
placeholder?: string
rows?: number
formatted?: boolean
[key: string]: unknown
}
access?: FieldAccess
hooks?: FieldHooks<unknown, unknown>
defaultValue?: unknown
})
Options
validation
Validation rules for the JSON field.
Type: object
Properties:
isRequired?: boolean- Field is required on create
ui.placeholder
Placeholder text for the JSON input.
Type: string
ui.rows
Number of rows for textarea display.
Type: number
ui.formatted
Whether to format JSON with indentation.
Type: boolean Default: true
Usage Example
metadata: json({
validation: { isRequired: false },
ui: {
placeholder: 'Enter JSON data...',
rows: 10,
formatted: true,
},
})
// Creating with JSON data
const item = await context.db.item.create({
data: {
metadata: {
tags: ['tag1', 'tag2'],
settings: { theme: 'dark', notifications: true },
},
},
})
// Querying returns parsed JSON
const item = await context.db.item.findUnique({
where: { id: '...' },
})
console.log(item.metadata.tags) // ['tag1', 'tag2']
Database Type
Prisma: Json (native JSON in PostgreSQL/MySQL, TEXT in SQLite)
TypeScript Type
unknown (requires type assertion or type guard when using)
virtual()
Computed field that is not stored in the database.
import { virtual } from '@opensaas/stack-core/fields'
virtual(options: {
type: string
hooks: {
resolveOutput: (args: {
operation: 'query'
value: unknown
item: TItem
listKey: string
fieldName: string
context: AccessContext
}) => unknown
resolveInput?: (args: {
operation: 'create' | 'update'
inputValue: unknown
item?: TItem
listKey: string
fieldName: string
context: AccessContext
}) => Promise<unknown> | unknown
}
ui?: {
[key: string]: unknown
}
access?: FieldAccess
})
Options
type (required)
TypeScript type for the virtual field output. Supports three formats:
Type: string | TypeDescriptor
Format 1: Primitive type strings (for built-in JavaScript types):
fullName: virtual({
type: 'string', // or 'number', 'boolean', 'Date', etc.
hooks: {
resolveOutput: ({ item }) => `${item.firstName} ${item.lastName}`,
},
})
Format 2: Import strings (for custom types):
import Decimal from 'decimal.js'
totalPrice: virtual({
type: "import('decimal.js').Decimal",
hooks: {
resolveOutput: ({ item }) => new Decimal(item.price).times(item.quantity),
},
})
Format 3: Type descriptor objects (recommended for custom types):
import Decimal from 'decimal.js'
totalPrice: virtual({
type: { value: Decimal, from: 'decimal.js' },
hooks: {
resolveOutput: ({ item }) => new Decimal(item.price).times(item.quantity),
},
})
// With custom name (when constructor name doesn't match export)
customField: virtual({
type: {
value: MyClass,
from: '@myorg/types',
name: 'MyExportedType', // Optional
},
hooks: {
resolveOutput: ({ item }) => new MyClass(item.data),
},
})
TypeDescriptor interface:
type TypeDescriptor =
| string // Primitive or import string
| {
value: new (...args: any[]) => any // Constructor/class
from: string // Import path
name?: string // Optional custom name
}
Examples:
'string'- For string values'number'- For numeric values'boolean'- For boolean values'string[]'- For arrays"import('decimal.js').Decimal"- For Decimal type{ value: Decimal, from: 'decimal.js' }- Type descriptor for Decimal
The TypeScript type generator automatically collects and generates the necessary import statements when using import strings or type descriptors.
Use cases:
- Financial calculations: Use
Decimalfromdecimal.jsfor precise currency calculations - Custom data structures: Return domain-specific types from virtual fields
- Third-party libraries: Integrate types from any npm package
hooks.resolveOutput (required)
Compute the field value from other fields in the item.
Type: Function
Parameters:
operation- Always'query'for virtual fieldsvalue- Database value (alwaysundefinedfor virtual fields)item- The full item with all selected fieldslistKey- The list name (e.g.,'User')fieldName- The field name (e.g.,'fullName')context- Access context with session and db
Returns: Computed value of the type specified in type option
Example:
displayName: virtual({
type: 'string',
hooks: {
resolveOutput: ({ item }) => {
return `${item.name} (${item.email})`
},
},
})
hooks.resolveInput (optional)
Perform side effects during create/update operations.
Type: Function (optional)
Parameters:
operation- Either'create'or'update'inputValue- Input value provided (if any)item- Existing item (undefined for create)listKey- The list namefieldName- The field namecontext- Access context
Returns: undefined (return value is ignored, use for side effects only)
Use cases:
- Sync data to external API
- Trigger webhooks
- Update related records
Example:
syncToExternal: virtual({
type: 'boolean',
hooks: {
resolveInput: async ({ item, operation }) => {
// Side effect: sync to external API
if (operation === 'update') {
await syncToExternalAPI(item)
}
return undefined // Return value is ignored
},
resolveOutput: () => true,
},
})
Key Characteristics
- No Database Storage: Virtual fields do not create database columns
- On-Demand Computation: Only computed when explicitly selected/included
- Type Safety: TypeScript type is generated from
typeoption - Performance: Efficient - only computed for requested fields
- Flexible: Can combine multiple fields or perform complex computations
Usage Examples
Read-Only Computed Field
User: list({
fields: {
firstName: text(),
lastName: text(),
fullName: virtual({
type: 'string',
hooks: {
resolveOutput: ({ item }) => `${item.firstName} ${item.lastName}`,
},
}),
},
})
// Usage
const user = await context.db.user.findUnique({
where: { id },
select: { firstName: true, lastName: true, fullName: true },
})
console.log(user.fullName) // "John Doe"
Complex Computed Value
Order: list({
fields: {
items: json(), // Array of { price: number, quantity: number }
total: virtual({
type: 'number',
hooks: {
resolveOutput: ({ item }) => {
if (!item.items || !Array.isArray(item.items)) return 0
return item.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
},
}),
},
})
Write Side Effects
Post: list({
fields: {
title: text(),
content: text(),
searchIndexSync: virtual({
type: 'boolean',
hooks: {
resolveInput: async ({ item, operation }) => {
// Update search index when post is created or updated
if (operation === 'create' || operation === 'update') {
await updateSearchIndex(item)
}
return undefined
},
resolveOutput: () => true,
},
}),
},
})
Important Notes
Virtual fields must be explicitly selected in queries. They are not included by default.
// ❌ Virtual field NOT computed
const user = await context.db.user.findUnique({
where: { id },
})
// user.fullName is undefined
// ✅ Virtual field IS computed
const user = await context.db.user.findUnique({
where: { id },
select: { firstName: true, lastName: true, fullName: true },
})
// user.fullName is "John Doe"
The resolveOutput hook must be provided. Virtual fields cannot exist without a computation function.
Database Type
None - virtual fields do not create database columns
TypeScript Type
Type specified in type option
Validation
Virtual fields do not accept input and cannot be validated. The getZodSchema method returns z.never().
Common Field Options
All field types support these common options:
access
Field-level access control rules.
Type: FieldAccess
type FieldAccess = {
read?: AccessControl
create?: AccessControl
update?: AccessControl
}
type AccessControl = (args: {
session: Session
item?: T
context: AccessContext
}) => boolean | Promise<boolean>
Example:
internalNotes: text({
access: {
read: ({ session }) => session?.role === 'admin',
create: ({ session }) => session?.role === 'admin',
update: ({ session }) => session?.role === 'admin',
},
})
See: Access Control guide for details
hooks
Field-level hooks for data transformation and side effects.
Type: FieldHooks<TInput, TOutput>
type FieldHooks<TInput, TOutput> = {
resolveInput?: (args: {
operation: 'create' | 'update'
inputValue: TInput | undefined
item?: TItem
listKey: string
fieldName: string
context: AccessContext
}) => Promise<TInput | undefined> | TInput | undefined
resolveOutput?: (args: {
operation: 'query'
value: TInput | undefined
item: TItem
listKey: string
fieldName: string
context: AccessContext
}) => TOutput | undefined
beforeOperation?: (args: {
operation: 'create' | 'update' | 'delete'
resolvedValue: TInput | undefined
item?: TItem
listKey: string
fieldName: string
context: AccessContext
}) => Promise<void> | void
afterOperation?: (args: {
operation: 'create' | 'update' | 'delete' | 'query'
value: TInput | TOutput | undefined
item: TItem
listKey: string
fieldName: string
context: AccessContext
}) => Promise<void> | void
}
Hook Types
resolveInput
Transform field value before database write.
When called: During create and update operations, after list-level resolveInput
Use cases: Hash passwords, normalize data, compute derived values
Example:
slug: text({
hooks: {
resolveInput: async ({ inputValue, item }) => {
// Auto-generate slug from title if not provided
if (!inputValue && item?.title) {
return item.title.toLowerCase().replace(/\s+/g, '-')
}
return inputValue
},
},
})
resolveOutput
Transform field value after database read.
When called: During query operations, after field-level access control
Use cases: Wrap sensitive data, format values, compute client-safe representations
Example:
profileImage: text({
hooks: {
resolveOutput: ({ value }) => {
// Add CDN prefix to image URLs
return value ? `https://cdn.example.com/${value}` : null
},
},
})
beforeOperation
Side effects before database operation. Does NOT modify data.
When called: Before create, update, or delete operations, after validation
Use cases: Logging, validation, pre-operation checks
Example:
status: select({
options: [
/* ... */
],
hooks: {
beforeOperation: async ({ operation, resolvedValue, item }) => {
// Log status changes
if (operation === 'update' && item.status !== resolvedValue) {
await auditLog.record({
event: 'status_change',
from: item.status,
to: resolvedValue,
})
}
},
},
})
afterOperation
Side effects after database operation. Does NOT modify data.
When called: After create, update, delete, or query operations
Use cases: Cache invalidation, webhooks, cleanup
Example:
thumbnail: text({
hooks: {
afterOperation: async ({ operation, value, item }) => {
if (operation === 'delete' && value) {
// Delete file from storage
await deleteFromCDN(value)
} else if (operation === 'update' && value) {
// Invalidate CDN cache
await invalidateCDNCache(value)
}
},
},
})
See: Hooks guide for execution order and patterns
ui
UI-specific configuration passed to field components.
Type: object
Common properties:
component?: React.Component- Custom field component (per-field override)fieldType?: string- Reference to globally registered field typevalueForClientSerialization?: (args) => unknown- Transform value before sending to browser- Additional field-type-specific options
Example:
content: text({
ui: {
displayMode: 'textarea',
placeholder: 'Enter your content...',
rows: 10,
// Custom UI options
spellcheck: true,
autocomplete: 'off',
},
})
Custom Component Example:
import { SlugField } from './components/SlugField'
slug: text({
ui: {
component: SlugField, // Use custom component for this field only
},
})
See: Custom Fields guide for creating custom components
defaultValue
Default value when creating new items.
Type: Varies by field type
Example:
status: select({
options: [
/* ... */
],
defaultValue: 'draft',
})
score: integer({ defaultValue: 0 })
isPublished: checkbox({ defaultValue: false })
createdAt: timestamp({ defaultValue: { kind: 'now' } })
Field Builder Methods
Every field configuration object implements these methods used by generators and validators:
getZodSchema(fieldName, operation)
Returns Zod schema for input validation.
Signature:
getZodSchema(
fieldName: string,
operation: 'create' | 'update'
): z.ZodTypeAny
Parameters:
fieldName- Field name (used in error messages)operation- Whether this is a create or update operation
Returns: Zod schema for validating input
Example implementation:
getZodSchema: (fieldName, operation) => {
const baseSchema = z.string({
message: `${fieldName} must be text`,
})
const withValidation = options?.validation?.isRequired
? baseSchema.min(1, { message: `${fieldName} is required` })
: baseSchema.optional()
return withValidation
}
getPrismaType(fieldName)
Returns Prisma type and modifiers for schema generation.
Signature:
getPrismaType(fieldName: string): {
type: string
modifiers?: string
}
Parameters:
fieldName- Field name (used for generating field-specific modifiers)
Returns: Object with:
type- Prisma scalar type (String,Int,Boolean,DateTime,Json)modifiers- Optional Prisma modifiers (?,@default(...),@unique,@index)
Example implementation:
getPrismaType: (fieldName) => {
return {
type: 'String',
modifiers: options?.validation?.isRequired ? undefined : '?',
}
}
getTypeScriptType()
Returns TypeScript type information for type generation.
Signature:
getTypeScriptType(): {
type: string
optional: boolean
}
Returns: Object with:
type- TypeScript type string (e.g.,'string','number','boolean','Date')optional- Whether the field is optional in TypeScript
Example implementation:
getTypeScriptType: () => {
return {
type: 'string',
optional: !options?.validation?.isRequired,
}
}
getTypeScriptImports()
Returns TypeScript imports needed for this field's type (optional).
Signature:
getTypeScriptImports(): Array<{
names: string[]
from: string
typeOnly?: boolean
}>
Returns: Array of import specifications
Example implementation:
getTypeScriptImports: () => {
return [
{
names: ['HashedPassword'],
from: '@opensaas/stack-core',
typeOnly: false,
},
]
}
Creating Custom Field Types
Custom field types must implement the BaseFieldConfig interface:
import type { BaseFieldConfig } from '@opensaas/stack-core'
import { z } from 'zod'
export type EmailField = BaseFieldConfig & {
type: 'email'
requireVerification?: boolean
}
export function email(options?: Omit<EmailField, 'type'>): EmailField {
return {
type: 'email',
...options,
getZodSchema: (fieldName, operation) => {
const schema = z.string().email({
message: `${fieldName} must be a valid email`,
})
return options?.validation?.isRequired ? schema : schema.optional()
},
getPrismaType: (fieldName) => {
return {
type: 'String',
modifiers: options?.validation?.isRequired ? undefined : '?',
}
},
getTypeScriptType: () => {
return {
type: 'string',
optional: !options?.validation?.isRequired,
}
},
}
}
Key principles:
- Extend
BaseFieldConfigwith your field's options - Implement all three generator methods
- Use field-level hooks for data transformation
- Field types are self-contained (no switch statements in core)
See: Custom Fields guide for complete tutorial
Third-Party Field Types
Rich Text (@opensaas/stack-tiptap)
import { richText } from '@opensaas/stack-tiptap/fields'
content: richText({
ui: {
minHeight: 300,
maxHeight: 800,
placeholder: 'Write your content...',
},
})
See: Tiptap package documentation
Image & File (@opensaas/stack-storage)
import { image, file } from '@opensaas/stack-storage/fields'
avatar: image({
storage: 's3',
validation: { isRequired: true },
transformations: {
thumbnail: { width: 150, height: 150 },
large: { width: 1200, height: 1200 },
},
})
document: file({
storage: 'local',
validation: {
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['application/pdf', 'text/plain'],
},
})
See: Storage package documentation
Validation System
Validation Rules
Field validation is defined in the validation object:
text({
validation: {
isRequired: true,
length: { min: 3, max: 100 },
},
})
integer({
validation: {
isRequired: true,
min: 0,
max: 100,
},
})
Validation Errors
Validation errors include:
- Field name (formatted for display)
- Error message
- Validation rule that failed
Example error:
{
"field": "title",
"message": "Title must be at least 3 characters"
}
Validation Execution Order
- Field-level Zod schema validation (from
getZodSchema()) - List-level
validateInputhook - Field-level access control (filter writable fields)
Best Practices
1. Choose Appropriate Field Types
// ✅ Good: Use integer for numbers
age: integer({ validation: { min: 0, max: 150 } })
// ❌ Bad: Don't use text for numbers
age: text({ validation: { length: { max: 3 } } })
2. Always Add Validation
// ✅ Good: Validate required fields and constraints
email: text({
isIndexed: 'unique',
validation: {
isRequired: true,
length: { max: 255 },
},
})
// ❌ Bad: No validation
email: text()
3. Use Relationships for Foreign Keys
// ✅ Good: Use relationship field
author: relationship({ ref: 'User.posts' })
// ❌ Bad: Don't manually manage IDs with text
authorId: text()
4. Add Indexes for Query Performance
// ✅ Good: Index fields used in queries
email: text({
isIndexed: 'unique',
validation: { isRequired: true },
})
slug: text({
isIndexed: true,
})
5. Use Hooks for Transformation
// ✅ Good: Use hooks for data transformation
email: text({
hooks: {
resolveInput: async ({ inputValue }) => {
return inputValue?.toLowerCase().trim()
},
},
})
// ❌ Bad: Don't transform in application code
Next Steps
- Field Types Guide - Usage examples and patterns
- Hooks System - Field-level data transformation
- Access Control - Field-level security
- Custom Fields Guide - Create custom field types
- Config API - Complete configuration reference