Skip to main content

@have/profiles

Historical document — written against a pre-rename API. This RFC was authored for @have/profiles before the @have/* → @happyvertical/* rename; the package ships today as @happyvertical/smrt-profiles. The per-type field helpers used throughout (text(), boolean(), datetime(), json()) were removed in #318 — schema now comes from TypeScript property types, narrowed by the field / foreignKey / meta decorators — and the implemented package has since grown beyond this spec (auth and identity models, profile assets, manifest generation). For the current surface, read packages/profiles/README.md and packages/profiles/AGENTS.md. This page is kept as a record of design intent, not as instructions.

Package Overview​

The @have/profiles package provides a comprehensive, flexible system for managing profiles, relationships, and metadata. It is designed as a SMRT-specific module (similar to @have/content and @have/products), excluded from the main SDK build pipeline but deeply integrated with the SMRT framework and other SDK packages.

Package Type: SMRT-specific module Location: packages/profiles/ Build Tool: Vite with smrtPlugin Target: Node.js only Dependencies: @have/smrt, @have/utils, and potentially extends @have/sql

Design Notes​

This document outlines the database schema for the @have/profiles package. The architecture is designed to be flexible, consistent, and extensible.

The key architectural decisions are:

  1. A Central profiles Table: A single table holds all primary entities, linked to a profile_types table to define their nature.

  2. Controlled EAV for Metadata: We use a controlled Entity-Attribute-Value model. The profile_metafields table defines a vocabulary of allowed keys, ensuring data consistency, while the profile_metadata table stores the values. This balances flexibility with integrity.

  3. Primary Key Strategy: UUID-based primary keys with unique slug fields (standard SmrtObject pattern)

    • All tables use UUID id as the primary key
    • Lookup tables (ProfileType, ProfileMetafield, ProfileRelationshipType) have unique slug fields for human-readable lookups
    • Convenience methods provided for slug-based access (e.g., ProfileType.getBySlug())
    • Future Enhancement: Slug-based primary keys tracked in issue #129
  4. Configurable Reciprocal Relationships: The relationship system allows developers to define custom reciprocal handlers for different relationship types. Built-in handlers provided for common patterns (friend, spouse, colleague), with ability to register custom handlers per application needs.

  5. Contextual & Temporal Relationships: Relationships can have a context_profile_id for creating tertiary links (e.g., two colleagues linked via a company). The profile_relationship_terms table adds a time dimension, crucial for tracking employment history, memberships, etc.

  6. Multi-Layer API: Comprehensive API design with instance methods, static methods, utility functions, and collection methods to avoid code duplication while providing flexibility.

  7. Lightweight Validation: Basic validation structure (type checking, regex, min/max) with extensibility points for applications to add custom validators via ProfileMetafield.validation JSON field.


Usage Examples​

Below are examples of how to model common scenarios. Note that slugs like 'org', 'human', 'job-title', and 'age' are assumed to be predefined in the profile_types and profile_metafields tables.

Example 1: Modeling an Organization​

// 1. Create the organization profile
const acmeInc = await createProfile({ typeSlug: 'org', name: 'ACME Inc.' });

// 2. Create profiles for two employees
const john = await createProfile({ typeSlug: 'human', name: 'John Doe', email: 'john.d@acme.inc' });
const jane = await createProfile({ typeSlug: 'human', name: 'Jane Smith', email: 'jane.s@acme.inc' });

// 3. Add metadata to the employees using the metafield slug
await setMetadata(john.id, 'job-title', 'Senior Engineer');
await setMetadata(jane.id, 'job-title', 'Product Manager');

// 4. Define the 'employee' relationship (directional)
await addRelationship(john.id, acmeInc.id, 'employee');
await addRelationship(jane.id, acmeInc.id, 'employee');

// 5. Define a 'colleague' relationship (reciprocal) between John and Jane,
// using the organization as the context.
await addRelationship(john.id, jane.id, 'colleague', acmeInc.id);

Example 2: Modeling a Family​

// 1. Create profiles for the family members
const dad = await createProfile({ typeSlug: 'human', name: 'Homer Simpson' });
const mom = await createProfile({ typeSlug: 'human', name: 'Marge Simpson' });
const child = await createProfile({ typeSlug: 'human', name: 'Bart Simpson' });

// 2. Add metadata using the metafield slugs
await setMetadata(dad.id, 'age', 39);
await setMetadata(mom.id, 'age', 36);
await setMetadata(child.id, 'age', 10);

// 3. Define the parent-child relationships (directional)
await addRelationship(dad.id, child.id, 'father');
await addRelationship(mom.id, child.id, 'mother');

// 4. Define the spouse relationship (reciprocal)
await addRelationship(dad.id, mom.id, 'spouse');

SMRT Integration​

The @have/profiles data model can be seamlessly integrated with the @have/smrt framework. By modeling the schema as SmrtObjects, we can leverage AI-powered operations, automatic code generation, and a rich object-oriented interface.

SMRT Object Definitions​

Here's how the core tables can be represented as SmrtObject classes using UUID primary keys:

import { SmrtObject, SmrtCollection } from '@have/smrt';
import { text, foreignKey, oneToMany, boolean, datetime, json } from '@have/smrt/fields';

// Represents the profile_types table
class ProfileType extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
slug = text({ unique: true, required: true }); // Unique slug for lookups
name = text({ required: true });
description = text();

// Convenience method for slug-based lookup
static async getBySlug(slug: string): Promise<ProfileType | null> {
const collection = await ProfileTypeCollection.create();
return await collection.get({ slug });
}
}

// Represents the profile_metafields table
class ProfileMetafield extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
slug = text({ unique: true, required: true }); // Unique slug for lookups
name = text({ required: true });
description = text();
validation = json(); // JSON validation schema

// Convenience method for slug-based lookup
static async getBySlug(slug: string): Promise<ProfileMetafield | null> {
const collection = await ProfileMetafieldCollection.create();
return await collection.get({ slug });
}
}

// Represents the profiles table
class Profile extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
typeId = foreignKey(ProfileType, { required: true }); // References ProfileType.id
email = text({ unique: true });
name = text({ required: true });
description = text();

// Relationships
metadata = oneToMany(ProfileMetadata);
relationshipsFrom = oneToMany(ProfileRelationship, { foreignKey: 'fromProfileId' });
relationshipsTo = oneToMany(ProfileRelationship, { foreignKey: 'toProfileId' });

// Convenience method to get type slug
async getTypeSlug(): Promise<string> {
const type = await this.loadRelated('typeId');
return type?.slug || '';
}

// Convenience method to set type by slug
async setTypeBySlug(slug: string): Promise<void> {
const type = await ProfileType.getBySlug(slug);
if (!type) throw new Error(`Profile type '${slug}' not found`);
this.typeId = type.id;
}

// AI-powered methods
async isMatch(criteria: string) {
return await this.is(criteria);
}

async generateBio() {
return await this.do('Write a short, professional bio for this person.');
}
}

// Represents the profile_metadata table
class ProfileMetadata extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
profileId = foreignKey(Profile, { required: true });
metafieldId = foreignKey(ProfileMetafield, { required: true });
value = text({ required: true });
}

// Represents the profile_relationship_types table
class ProfileRelationshipType extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
slug = text({ unique: true, required: true }); // Unique slug for lookups
name = text({ required: true });
reciprocal = boolean({ default: true });

// Convenience method for slug-based lookup
static async getBySlug(slug: string): Promise<ProfileRelationshipType | null> {
const collection = await ProfileRelationshipTypeCollection.create();
return await collection.get({ slug });
}
}

// Represents the profile_relationships table
class ProfileRelationship extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
fromProfileId = foreignKey(Profile, { required: true });
toProfileId = foreignKey(Profile, { required: true });
typeId = foreignKey(ProfileRelationshipType, { required: true });
contextProfileId = foreignKey(Profile); // Optional tertiary context
terms = oneToMany(ProfileRelationshipTerm);
}

// Represents the profile_relationship_terms table
class ProfileRelationshipTerm extends SmrtObject {
// id: UUID (auto-generated by SmrtObject)
relationshipId = foreignKey(ProfileRelationship, { required: true });
startedAt = datetime({ required: true });
endedAt = datetime();
}

SMRT Collection Classes​

We can define SmrtCollection classes to manage these objects:

class ProfileCollection extends SmrtCollection<Profile> {
static readonly _itemClass = Profile;
}

class ProfileTypeCollection extends SmrtCollection<ProfileType> {
static readonly _itemClass = ProfileType;
}

// ... and so on for other objects

Benefits of SMRT Integration​

  • Object-Oriented API: Interact with profiles as objects with methods, not just database rows.
  • AI Capabilities: Use is() and do() methods on profiles for advanced matching and content generation.
  • Code Generation: Automatically generate CLIs, REST APIs, and MCP servers for the entire profile system.
  • Simplified Queries: Use the SmrtCollection query interface instead of writing raw SQL.
  • Data Validation: Leverage the smrt field system for automatic data validation.

Core Tables​

profile_types​

A lookup table that defines the nature of a profile.

  • slug: (String, Primary Key) - A unique, human-readable identifier (e.g., 'human', 'org', 'robot').
  • name: (String) - A user-friendly display name (e.g., 'Human', 'Organization').
  • description: (Text, Nullable) - A brief explanation of the profile type.

profile_metafields​

A lookup table that defines the controlled vocabulary for metadata keys.

  • slug: (String, Primary Key) - A unique, human-readable identifier (e.g., 'job-title', 'age').
  • name: (String) - A user-friendly display name (e.g., 'Job Title', 'Age').
  • description: (Text, Nullable) - A brief explanation of the field.
  • validation: (JSON, Nullable) - Rules for validating the field's value (e.g., regex, min/max). For example, for an 'age' metafield, the validation might be {"type": "integer", "min": 0}.

profiles​

Stores the core information for any entity.

  • id: (UUID, Primary Key) - The unique identifier for the profile.
  • type_slug: (String, Foreign Key to profile_types.slug) - The type of entity this profile represents.
  • email: (String, Nullable, Unique) - The primary email address.
  • name: (String) - The display name for the profile.
  • description: (Text, Nullable) - A short bio or description.
  • created_at: (Datetime)
  • updated_at: (Datetime)

profile_metadata​

Stores the actual metadata values for each profile, linked to a defined metafield.

  • id: (UUID, Primary Key) - The unique identifier for the metadata entry.
  • profile_id: (UUID, Foreign Key to profiles.id) - The profile this metadata belongs to.
  • metafield_slug: (String, Foreign Key to profile_metafields.slug) - The metadata key.
  • value: (Text) - The metadata value.
  • created_at: (Datetime)
  • updated_at: (Datetime)

Relationship Tables​

profile_relationship_types​

A lookup table that defines the kinds of relationships that can exist.

  • slug: (String, Primary Key) - A unique, human-readable identifier (e.g., 'friend', 'father', 'employee').
  • name: (String, Unique) - A user-friendly display name (e.g., 'Friend', 'Father').
  • reciprocal: (Boolean, Default: true) - true for two-way relationships ('friend'), false for one-way ('father').

profile_relationships​

Connects two profiles together under a specific relationship type.

  • id: (UUID, Primary Key) - The unique identifier for the relationship instance.
  • from_profile_id: (UUID, Foreign Key to profiles.id) - The origin profile.
  • to_profile_id: (UUID, Foreign Key to profiles.id) - The target profile.
  • type_slug: (String, Foreign Key to profile_relationship_types.slug) - The type of relationship.
  • context_profile_id: (UUID, Foreign Key to profiles.id, Nullable) - The context for a tertiary relationship.
  • created_at: (Datetime)

profile_relationship_terms​

Defines the duration or terms of a specific relationship.

  • id: (UUID, Primary Key) - The unique identifier for the term.
  • relationship_id: (UUID, Foreign Key to profile_relationships.id) - The relationship this term applies to.
  • started_at: (Datetime)
  • ended_at: (Datetime, Nullable)
  • created_at: (Datetime)
  • updated_at: (Datetime)

Data Integrity and Performance​

To ensure data consistency and optimal query performance, the following considerations should be implemented:

  • Cascading Deletes: Foreign key constraints should be configured with ON DELETE CASCADE. For instance, if a profile is deleted, all its associated metadata, relationships, and relationship terms should be automatically removed from the database. This maintains referential integrity.

  • Uniqueness Constraints:

    • In the profile_metadata table, a unique constraint should be placed on the combination of (profile_id, metafield_slug) to ensure that each profile has only one value for a given metafield.
    • In the profile_relationships table, a unique constraint on (from_profile_id, to_profile_id, type_slug, context_profile_id) is recommended to prevent the creation of duplicate relationships.
  • Indexing: For faster data retrieval, indexes should be created on all foreign key columns. This includes type_slug in profiles, profile_id and metafield_slug in profile_metadata, and the various ID columns in the relationship tables.


Core Functions​

Metadata Functions​

  • getMetadata(profileId): Retrieves all metadata for a given profile as a key-value object.
  • setMetadata(profileId, metafieldSlug, value): Creates or updates a single metadata value for a profile.
  • updateMetadata(profileId, metadataObject): Updates multiple metadata values from an object where keys are metafield slugs.
  • findProfilesByMeta(metafieldSlug, value): Finds all profiles with a specific metadata key-value pair.

Relationship Functions​

  • addRelationship(fromProfileId, toProfileId, relationshipSlug, contextProfileId=None): Creates a relationship. If the relationship type is reciprocal, the system automatically creates the corresponding inverse relationship (i.e., from toProfileId to fromProfileId).
  • removeRelationship(fromProfileId, toProfileId, relationshipSlug): Removes a relationship. If the relationship is reciprocal, the inverse relationship is also automatically removed.
  • getRelationships(profileId, direction='all'): Retrieves all relationships for a profile.
  • getRelatedProfiles(profileId, relationshipSlug=None): Retrieves profiles that have a specific relationship with the given profile.

Relationship Term Functions​

  • addTermToRelationship(relationshipId, startedAt, endedAt=None): Adds a term to a relationship.
  • endRelationshipTerm(termId, endedAt): Ends an active relationship term.
  • getRelationshipHistory(relationshipId): Retrieves all terms for a relationship.

Reciprocal Relationship System​

The @have/profiles package provides a flexible system for handling reciprocal (two-way) relationships. Each ProfileRelationshipType can define whether it's reciprocal, and optionally specify a custom handler function for creating the inverse relationship.

Handler Function Interface​

interface ReciprocalHandler {
(
from: Profile,
to: Profile,
context?: Profile,
options?: any
): Promise<void>;
}

Built-in Reciprocal Handlers​

The package provides default handlers for common relationship patterns:

const DEFAULT_HANDLERS: Record<string, ReciprocalHandler> = {
// Symmetric relationships (same type in both directions)
friend: async (from, to, context) => {
await to.addRelationship(from, 'friend', context);
},

spouse: async (from, to) => {
await to.addRelationship(from, 'spouse');
},

partner: async (from, to, context) => {
await to.addRelationship(from, 'partner', context);
},

colleague: async (from, to, context) => {
await to.addRelationship(from, 'colleague', context);
},

sibling: async (from, to) => {
await to.addRelationship(from, 'sibling');
},
};

Custom Handler Registration​

Applications can register custom reciprocal handlers:

import { ProfileRelationshipType } from '@have/profiles';

// Register a custom handler
ProfileRelationshipType.registerReciprocalHandler(
'business-partner',
async (from, to, context) => {
// Custom logic for creating inverse relationship
await to.addRelationship(from, 'business-partner', context);

// Additional business logic (e.g., create contract record)
await createPartnershipContract(from, to);
}
);

Asymmetric Reciprocal Relationships​

For relationships with different inverse types (e.g., mentor/mentee, parent/child):

ProfileRelationshipType.registerReciprocalHandler(
'mentor',
async (from, to, context) => {
// When A mentors B, B is mentee of A
await to.addRelationship(from, 'mentee', context);
}
);

ProfileRelationshipType.registerReciprocalHandler(
'parent',
async (from, to) => {
// When A is parent of B, B is child of A
await to.addRelationship(from, 'child');
}
);

API Layer Architecture​

The package provides a comprehensive, multi-layer API to avoid code duplication while offering maximum flexibility.

1. Instance Methods​

Direct manipulation on profile objects:

const profile = await profiles.get({ id: 'profile-123' });

// Metadata operations
await profile.addMetadata('job-title', 'Senior Engineer');
const metadata = await profile.getMetadata(); // { 'job-title': 'Senior Engineer', ... }
await profile.updateMetadata({ 'job-title': 'Lead Engineer', 'department': 'Engineering' });
await profile.removeMetadata('department');

// Relationship operations
const colleague = await profiles.get({ id: 'profile-456' });
const company = await profiles.get({ id: 'org-789' });
await profile.addRelationship(colleague, 'colleague', company);
const relationships = await profile.getRelationships({ typeSlug: 'colleague' });
const relatedProfiles = await profile.getRelatedProfiles('colleague');
await profile.removeRelationship(colleague, 'colleague');

// AI-powered operations
const bio = await profile.generateBio();
const isEngineer = await profile.matches('works as a software engineer');

2. Static Methods​

Class-level queries:

// Find profiles by metadata
const engineers = await Profile.findByMetadata('job-title', 'Senior Engineer');

// Find by profile type
const organizations = await Profile.findByType('org');

// Find related profiles
const colleagues = await Profile.findRelated('profile-123', 'colleague');

// Search by email
const user = await Profile.searchByEmail('john@example.com');

// Relationship type utilities
const isReciprocal = await ProfileRelationshipType.getReciprocal('friend'); // true
ProfileRelationshipType.registerReciprocalHandler('custom', handlerFn);

3. Utility Functions​

Standalone helpers in utils.ts:

import {
getProfileMetadata,
setProfileMetadata,
findProfilesByMeta,
createReciprocalRelationship,
validateMetadataValue,
} from '@have/profiles/utils';

// Metadata utilities
const metadata = await getProfileMetadata('profile-123');
await setProfileMetadata('profile-123', 'age', 35);
const profiles = await findProfilesByMeta('department', 'Engineering');

// Relationship utilities
await createReciprocalRelationship(profileA, profileB, 'friend', context);

// Validation utilities
const isValid = await validateMetadataValue(metafield, value);

4. Collection Methods​

Extended batch operations:

const profiles = await ProfileCollection.create({ db: { url: 'sqlite:./profiles.db' } });

// Batch operations
const engineers = await profiles.findByType('human');
const metadata = await profiles.batchGetMetadata(['id1', 'id2', 'id3']);
await profiles.batchUpdateMetadata([
{ profileId: 'id1', data: { 'job-title': 'Engineer' } },
{ profileId: 'id2', data: { 'job-title': 'Designer' } },
]);

// Relationship queries
const related = await profiles.findRelated('profile-123', 'colleague');
const network = await profiles.getRelationshipNetwork('profile-123', { maxDepth: 2 });

Validation System​

The package provides a lightweight validation system with clear extension points for applications.

Validation Schema Structure​

The ProfileMetafield.validation JSON field supports the following structure:

interface ValidationSchema {
// Type constraint
type?: 'string' | 'number' | 'boolean' | 'date' | 'json';

// String constraints
pattern?: string; // Regex pattern
minLength?: number;
maxLength?: number;

// Numeric constraints
min?: number;
max?: number;

// Custom validator reference
custom?: string; // Name of registered custom validator function

// Error message
message?: string; // Custom validation error message
}

Built-in Validation​

Basic validation is performed automatically:

// Type validation
const ageField = await ProfileMetafield.create({
slug: 'age',
name: 'Age',
validation: { type: 'number', min: 0, max: 150 }
});

await profile.addMetadata('age', 35); // ✓ Valid
await profile.addMetadata('age', 'old'); // ✗ ValidationError: Expected number
await profile.addMetadata('age', -5); // ✗ ValidationError: Value below minimum
await profile.addMetadata('age', 200); // ✗ ValidationError: Value above maximum

// Pattern validation
const emailField = await ProfileMetafield.create({
slug: 'email',
name: 'Email',
validation: {
type: 'string',
pattern: '^[^@]+@[^@]+\\.[^@]+$',
message: 'Invalid email format'
}
});

await profile.addMetadata('email', 'john@example.com'); // ✓ Valid
await profile.addMetadata('email', 'invalid-email'); // ✗ ValidationError: Invalid email format

Custom Validator Registration​

Applications can register custom validation functions:

import { ProfileMetadata } from '@have/profiles';

// Register a custom validator
ProfileMetadata.registerValidator('us-phone', (value: any) => {
const phoneRegex = /^\d{3}-\d{3}-\d{4}$/;
return phoneRegex.test(value);
});

// Use in metafield definition
const phoneField = await ProfileMetafield.create({
slug: 'phone',
name: 'Phone Number',
validation: {
type: 'string',
custom: 'us-phone',
message: 'Phone must be in format XXX-XXX-XXXX'
}
});

Dependencies & SDK Integration​

The @have/profiles package maximizes integration with existing SDK packages while potentially extending them for specialized needs.

Core Dependencies​

@have/smrt (Required)​

  • Purpose: Core SMRT framework
  • Usage:
    • SmrtObject base class for all profile models
    • SmrtCollection for collection management
    • @smrt() decorator for API/MCP/CLI generation
    • Field definitions (text(), foreignKey(), boolean(), etc.)
    • AI-powered methods (do(), is())
    • Automatic schema generation and database triggers

@have/utils (Required)​

  • Purpose: Shared utilities
  • Usage:
    • makeSlug() for generating URL-friendly identifiers
    • generateId() for UUID generation (if needed)
    • Type utilities and helper functions

@have/sql (Potential Extension)​

  • Purpose: Database operations
  • Potential Extensions:
    • Enhanced query builders for complex relationship queries
    • Graph traversal utilities for relationship networks
    • Optimized batch operations for metadata updates
    • Recursive CTE helpers for hierarchical relationships

Proposed Extensions:

// Potential additions to @have/sql
export function buildGraphQuery(
fromTable: string,
relationshipTable: string,
maxDepth: number
): string {
// Recursive CTE for relationship traversal
}

export function buildBatchUpsert(
table: string,
records: any[],
uniqueKeys: string[]
): string {
// Efficient batch upsert for metadata updates
}

@have/ai (Indirect)​

  • Purpose: AI model interactions
  • Usage: Inherited via SmrtObject for AI-powered profile operations:
    • profile.generateBio() - Generate professional bios
    • profile.matches(criteria) - Semantic profile matching
    • profile.analyzeSentiment() - Analyze profile description sentiment
    • Custom AI-powered profile analysis methods

Optional Integration Points​

@have/files​

  • Use Case: Profile avatar/photo management
  • Potential Features:
    • Upload and store profile photos
    • Generate thumbnails
    • Manage document attachments (resumes, portfolios)

@have/content​

  • Use Case: Profile content management
  • Potential Features:
    • Rich-text profile descriptions
    • Blog posts or articles by profiles
    • Activity feeds and timeline content

Package Structure​

packages/profiles/
├── src/
│ ├── models/
│ │ ├── Profile.ts # Main profile model
│ │ ├── ProfileType.ts # Profile type lookup
│ │ ├── ProfileMetafield.ts # Metadata field definitions
│ │ ├── ProfileMetadata.ts # Metadata values
│ │ ├── ProfileRelationship.ts # Relationship instances
│ │ ├── ProfileRelationshipType.ts # Relationship type lookup
│ │ ├── ProfileRelationshipTerm.ts # Temporal relationship data
│ │ ├── __tests__/
│ │ │ ├── Profile.test.ts
│ │ │ ├── ProfileMetadata.test.ts
│ │ │ └── ProfileRelationship.test.ts
│ │ └── index.ts
│ ├── collections/
│ │ ├── ProfileCollection.ts
│ │ ├── ProfileTypeCollection.ts
│ │ ├── ProfileMetafieldCollection.ts
│ │ ├── ProfileMetadataCollection.ts
│ │ ├── ProfileRelationshipCollection.ts
│ │ ├── ProfileRelationshipTypeCollection.ts
│ │ ├── ProfileRelationshipTermCollection.ts
│ │ └── index.ts
│ ├── utils.ts # Utility functions
│ ├── utils.test.ts
│ ├── types.ts # TypeScript type definitions
│ └── index.ts # Main package export
├── package.json
├── tsconfig.json
├── vite.config.ts
├── vitest.config.ts
├── SPEC.md # This file
├── CLAUDE.md # Package documentation for AI agents
└── README.md # User-facing documentation

Implementation Phases​

Phase 1: Foundation​

  • Package infrastructure (package.json, build configs)
  • Core model definitions with SMRT decorators
  • Basic collection classes

Phase 2: Core Functionality​

  • Instance methods for metadata and relationship management
  • Static query methods
  • Utility functions

Phase 3: Advanced Features​

  • Reciprocal relationship handler system
  • Validation framework with custom validators
  • AI-powered profile methods

Phase 4: Testing & Documentation​

  • Comprehensive test coverage
  • CLAUDE.md package documentation
  • README.md user guide
  • Integration tests with other SDK packages

Implementation Decision: UUID Primary Keys​

After evaluation, we've decided to use UUID-based primary keys (standard SmrtObject pattern) for the initial implementation. This provides:

✅ Full compatibility with existing SMRT framework ✅ No special handling or framework modifications required ✅ Proven, stable implementation ✅ Allows profiles package to launch immediately

Slug-based convenience methods will be provided for human-readable lookups:

class ProfileType extends SmrtObject {
id = text(); // UUID PK (auto-generated)
slug = text({ unique: true }); // Unique slug for lookups
name = text({ required: true });
description = text();

// Convenience method for slug-based lookup
static async getBySlug(slug: string): Promise<ProfileType | null> {
const collection = await ProfileTypeCollection.create();
return await collection.get({ slug });
}
}

class Profile extends SmrtObject {
typeId = foreignKey(ProfileType, { required: true }); // References UUID
email = text({ unique: true });
name = text({ required: true });
description = text();

// Convenience method to get type slug
async getTypeSlug(): Promise<string> {
const type = await this.loadRelated('typeId');
return type?.slug || '';
}

// Convenience method to set type by slug
async setTypeBySlug(slug: string): Promise<void> {
const type = await ProfileType.getBySlug(slug);
if (!type) throw new Error(`Profile type '${slug}' not found`);
this.typeId = type.id;
}
}

Future Enhancement: Native slug-based primary key support is tracked in issue #129. The package can be migrated to use slug PKs once that feature is implemented in the SMRT framework.