SynapSynapDocs
DevelopmentExtending

Direct Plugins

Direct Plugins

Extend Data Pod by adding code directly to the repository


When to Use

✅ Core functionality everyone needs
✅ Tight integration with Data Pod internals
✅ Open source contribution
✅ Performance critical features

❌ Proprietary logic → Use Remote Plugin
❌ Heavy AI processing → Use Remote Plugin


How It Works

Add features by modifying Data Pod packages:

  1. Add API endpoint → Create tRPC router
  2. Add data storage → Create database schema
  3. Add events → Define typed domain events
  4. Add logic → Create event handlers

The SDK auto-updates with your changes.


Step-by-Step Tutorial

1. Add a Router

Create a new tRPC endpoint:

// packages/api/src/routers/my-feature.ts
import { router, protectedProcedure } from '../trpc.js';
import { z } from 'zod';
 
export const myFeatureRouter = router({
  create: protectedProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async ({ input, ctx }) => {
      // Your logic here
      return { success: true };
    }),
});

Register it:

// packages/api/src/index.ts
import { myFeatureRouter } from './routers/my-feature.js';
 
registerRouter('myFeature', myFeatureRouter, {
  version: '1.0.0',
  source: 'core',
  description: 'My feature'
});

Done! Frontend automatically gets typed access:

await client.rpc.myFeature.create.mutate({ name: 'test' });

2. Add Schema (Optional)

Need to store data? Add a table:

// packages/database/src/schema/my-feature.ts
import { pgTable, uuid, text } from 'drizzle-orm/pg-core';
 
export const myFeatureData = pgTable('my_feature_data', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: text('user_id').notNull(),
  name: text('name').notNull(),
});

Create migration:

-- packages/database/migrations/XXXX_my_feature.sql
CREATE TABLE my_feature_data (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id TEXT NOT NULL,
  name TEXT NOT NULL
);

Export from schema:

// packages/database/src/schema/index.ts
export * from './my-feature.js';

3. Add Events (Optional)

Need event sourcing? Define typed events:

// packages/events/src/domain-events.ts
 
// Define your event
export type MyFeatureCreatedEvent = BaseEvent<
  'myfeature.created',
  'myfeature_item',
  {
    name: string;
    metadata?: Record<string, unknown>;
  }
>;
 
// Add to union
export type DomainEvent =
  | InboxItemReceivedEvent
  | MyFeatureCreatedEvent  // ← Add here
  | ...;

Create builder:

// packages/events/src/publisher.ts
export function createMyFeatureCreatedEvent(
  itemId: string,
  data: EventDataFor<'myfeature.created'>
) {
  return {
    type: 'myfeature.created' as const,
    subjectId: itemId,
    subjectType: 'myfeature_item' as const,
    data,
  };
}

Publish events:

import { publishEvent, createMyFeatureCreatedEvent } from '@synap/events';
 
const event = createMyFeatureCreatedEvent(itemId, { name: 'test' });
await publishEvent(event, { userId: ctx.userId });

4. Add Event Handler (Optional)

Process events asynchronously:

// packages/api/src/event-handlers/my-feature.ts
import type { MyFeatureCreatedEvent } from '@synap/events';
 
export async function handleMyFeatureCreated(
  event: MyFeatureCreatedEvent & { userId: string }
) {
  // Process event
  // Update projections
  // Trigger side effects
}

Register handler:

// packages/api/src/event-handlers/index.ts
switch (event.type) {
  case 'myfeature.created':
    await handleMyFeatureCreated(event);
    break;
}

Real Example: Intelligence Registry

See how Intelligence Registry implements all these patterns:

  • Router: packages/api/src/routers/intelligence-registry.ts
  • Schema: packages/database/src/schema/intelligence-services.ts
  • Events: Not needed (simple CRUD)

View code →


Testing

# Run tests
pnpm test
 
# Test your router specifically
pnpm test -- my-feature

Building

# Build packages
cd packages/database && pnpm build
cd ../api && pnpm build
cd ../../apps/api && pnpm build

Type Safety

Every step is type-safe:

  • ✅ Schema → TypeScript types (Drizzle ORM)
  • ✅ Events → Typed domain events
  • ✅ Router → tRPC types
  • ✅ SDK → Auto-generated client types

Frontends get full autocomplete!


Next Steps

On this page