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.
Decision guide
Section titled “Decision guide”| You need… | Reach for |
|---|---|
| Custom logic in a generated CRUD route | Executor override |
| A new route inside the entity shell | Extra route |
| Full control over entity storage construction | Manual adapter wiring |
| Custom system field names for an existing database | Entity systemFields config |
| Custom storage conventions (Redis keys, table names) | Entity conventions config |
| Direct lifecycle control (async bootstrap, teardown) | SlingshotPlugin (Plugin Interface) |
Executor overrides
Section titled “Executor overrides”Replace the handler behind a generated route without changing its identity:
// @skip-typecheckimport { 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.
Extra routes
Section titled “Extra routes”Add routes that live alongside generated entity routes:
// @skip-typecheckimport { 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 }); }, }), ],});Manual adapter wiring
Section titled “Manual adapter wiring”Build your own storage adapter when the generated ones do not fit:
// @skip-typecheckentity({ 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.
Framework-level plugins
Section titled “Framework-level plugins”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-typecheckimport 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.
Entity system field conventions
Section titled “Entity system field conventions”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}`, },});The rule
Section titled “The rule”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:
- Start with packages, entities, and route policy — covers most app code
- Add overrides or extra routes — when generated behavior needs tweaking
- 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 Interface —
SlingshotPlugincontract for framework-level plugins - Entity System — the entity authoring and customization story
- Examples — complete apps that combine packages, entities, and custom routes