Skip to content

Lower-level Seams

definePackage and the entity system cover most app code. When generated behavior needs tweaking, the framework exposes seams at every level so you can override one piece without abandoning the rest.

For framework-level plugin authoring (auth, persistence, queues, custom middleware), see the Plugin Interface — that is a parallel first-class authoring tier, not a seam below packages.

You need…Reach for
Custom logic in a generated CRUD routeExecutor override
A new route inside the entity shellExtra route
Full control over entity storage constructionManual adapter wiring
Custom system field names for an existing databaseEntity systemFields config
Custom storage conventions (Redis keys, table names)Entity conventions config
Direct lifecycle control (async bootstrap, teardown)SlingshotPlugin (Plugin Interface)

Replace the handler behind a generated route without changing its identity:

// @skip-typecheck
import { defineEntityExecutor, entity } from '@lastshotlabs/slingshot-entity';
entity({
config: Post,
operations: PostOps,
overrides: {
get: defineEntityExecutor({
summary: 'Get post with view tracking',
build:
({ entityAdapter }) =>
async ({ params, respond }) => {
const post = await entityAdapter.get(params.id);
if (!post) return respond.notFound();
// Track the view
await entityAdapter.update(params.id, { views: (post.views ?? 0) + 1 });
return respond.json(post);
},
}),
},
});

See Generated Routes, Overrides, and Extra Routes for full details.

Add routes that live alongside generated entity routes:

// @skip-typecheck
import { defineEntityRoute, entity } from '@lastshotlabs/slingshot-entity';
entity({
config: Post,
extraRoutes: [
defineEntityRoute({
key: 'archive-all',
method: 'post',
path: '/archive-all',
auth: 'userAuth',
permission: { requires: 'blog:post.moderate' },
buildExecutor:
({ entityAdapter }) =>
async ({ respond }) => {
// Custom batch operation
const all = await entityAdapter.list({});
for (const post of all.items) {
await entityAdapter.update(post.id, { status: 'archived' });
}
return respond.json({ archived: all.items.length });
},
}),
],
});

Build your own storage adapter when the generated ones do not fit:

// @skip-typecheck
entity({
config: Post,
wiring: {
mode: 'manual',
buildAdapter: (storeType, infra) => {
// Use your own ORM, raw SQL, or external API
return {
get: async id => {
/* ... */
},
list: async opts => {
/* ... */
},
create: async data => {
/* ... */
},
update: async (id, data) => {
/* ... */
},
delete: async id => {
/* ... */
},
};
},
},
});

See Storage and Adapter Wiring for the factories and manual wiring modes.

SlingshotPlugin is the contract for framework-level plugins — auth, persistence, queues, custom middleware. It is a parallel authoring tier to definePackage, not a seam beneath it. If you find yourself needing async bootstrap, conditional middleware, or registrar registrations to model a feature, you are writing a plugin and should consult the full Plugin Interface reference.

A short example for context:

// @skip-typecheck
import type { SlingshotPlugin } from '@lastshotlabs/slingshot-core';
const metricsPlugin: SlingshotPlugin = {
name: 'metrics',
setupMiddleware({ app }) {
app.use('*', async (c, next) => {
const start = Date.now();
await next();
console.log(`${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - start}ms`);
});
},
};

See Plugin Interface for the full lifecycle and Composing an App for the package vs plugin decision guide.

Override generated field names to match an existing database schema:

const LegacyPost = defineEntity('LegacyPost', {
namespace: 'blog',
fields: {
id: field.string({ primary: true }),
title: field.string(),
},
systemFields: {
ownerField: 'author_id', // not 'authorId'
tenantField: 'organization_id', // not 'orgId'
},
storageFields: {
ttlField: '_expires_at',
},
storage: {
postgres: { tableName: 'blog_posts_v2' },
redis: { keyPrefix: 'bp' },
},
conventions: {
redisKey: ({ storageName, id }) => `${storageName}:v2:${id}`,
},
});

Stay on the package surface until you have a concrete reason to leave it. These seams exist so you never hit a wall — not so you start at the lowest level.

Typical progression for app-side feature work:

  1. Start with packages, entities, and route policy — covers most app code
  2. Add overrides or extra routes — when generated behavior needs tweaking
  3. Use capabilities and entityRef — when packages need to talk to each other

For framework-level work — auth, persistence, queues, custom middleware — start at the Plugin Interface instead. That’s a different tier, not a later step in the same progression.

  • Plugin InterfaceSlingshotPlugin contract for framework-level plugins
  • Entity System — the entity authoring and customization story
  • Examples — complete apps that combine packages, entities, and custom routes