Component Design
Components are the atoms of a Nebula application. Each component defines a strict JSON schema that Nebula validates on every create, read, and update operation. This isn't optional validation — if data doesn't match the schema, the operation fails.
Schema Definition
A component has a title, a description, and a JSON schema. The description supports both human readers (management, onboarding) and agentic use cases (AI assistants that need to understand your data model).
Descriptions at the field level matter. They're surfaced in the system editor, in SDK tooling, and in any AI-powered features that inspect your component library.
Evolution Without Breaking Changes
A fundamental design rule: components cannot have breaking schema changes. You can add optional fields. You can make required fields optional. But you cannot remove fields, change types, or add new required fields to an existing component.
This constraint is intentional. In a system where entities accumulate components over time and many systems process them independently, a breaking schema change would cascade failures across every system that reads that component.
Instead of migrating, you extend. Need a V2 of a component? Create a new one. DenialPriority and DenialPriorityV2 can coexist on the same entity. Systems migrate at their own pace — the old component doesn't disappear, and nothing breaks.
Rich Property Types
Beyond standard JSON types, Nebula schemas support a file property type. In the SDK, file properties become FileReference objects with methods for downloading, generating presigned URLs, and reading metadata.
// A component with a file property
interface MedicalRecordData extends IComponent {
documentType: string;
uploadedAt: string;
attachment: FileReference; // SDK wraps this automatically
}
// In your React component
function RecordViewer({ entity }) {
const record = entity.components.MedicalRecord;
const { url } = useFileUrl(record?.attachment);
return url ? <iframe src={url} /> : <p>Loading document...</p>;
}
Nebula maintains its own CDN alongside the entity data store, handling upload, storage, and delivery of binary assets. The SDK abstracts the CDN details — you work with FileReference objects and let the platform handle the rest.
TypeScript Component Registration
The full pattern for registering a component connects the interface, factory, and query fields:
import {
createComponentSimple,
Field,
type IComponent,
} from '@nebula/sdk';
// 1. Interface — defines the shape
interface DenialPriorityData extends IComponent {
daysRemaining: number | null;
totalValue: number;
urgencyScore: number;
compositeScore: number;
calculatedAt: string;
}
// 2. Factory — creates the component reference
const DenialPriority =
createComponentSimple<DenialPriorityData>('DenialPriority');
// 3. Fields — enables type-safe query building
const DenialPriorityFields = {
ComponentName: 'DenialPriority' as const,
TotalValue: new Field<DenialPriorityData, number>(
'DenialPriority', 'totalValue', 'number'
),
CompositeScore: new Field<DenialPriorityData, number>(
'DenialPriority', 'compositeScore', 'number'
),
};
This three-part pattern (interface, factory, fields) is consistent across every component in a Nebula application. The fields enable queries like gt(DenialPriorityFields.CompositeScore, 1000) — type-safe, autocomplete-friendly, and impossible to misspell.
Components are the unit of both data and Access Control. Each role policy maps directly to component-level CRUD permissions. For how components participate in automated processing, see Behaviors. For patterns that leverage components as query surface area and lightweight tags, see Component Query Refiners and Lightweight Tags.