Authoring Model
Every Slingshot app follows the same structure. Understanding it makes everything else click.
The three layers
Section titled “The three layers”defineApp() ← app root: assembly, config, security └── definePackage() ← packages: feature boundaries ├── entity() ← entities: generated CRUD + operations ├── domain() + route.*() ← routes: custom endpoints └── definePackageContract() ← public surface: typed capabilities + entity refs consumed by other packagesApp root — one file that assembles your app. Config covers ports, security, databases, secrets, and which packages to load.
Packages — each feature area is a package. A blog package has post entities and preview routes. An auth package has login routes and session middleware. Packages declare their dependencies and compose without coupling.
Entities — when a package owns data (posts, tasks, users, orders), define an entity.
Slingshot generates CRUD routes, validation, OpenAPI docs, and storage wiring. Add operations
for domain actions like publish or archive.
Package Contracts — when another package needs to consume yours, declare a typed public
surface (definePackageContract). The contract bundles the capabilities and narrowed entity
refs you publish, validates them at boot, and keeps the boundary explicit.
A real example
Section titled “A real example”// @skip-typecheckimport { defineApp, defineEntity, definePackage, domain, entity, field, route,} from '@lastshotlabs/slingshot';
// Entity: generated CRUD + operationsconst Post = defineEntity('Post', { namespace: 'blog', fields: { id: field.string({ primary: true, default: 'uuid' }), title: field.string(), body: field.string(), published: field.boolean({ default: false }), },});
// Package: owns the blog feature boundaryconst blogPackage = definePackage({ name: 'blog', entities: [entity({ config: Post })], domains: [ domain({ name: 'preview', basePath: '/preview', routes: [ route.get({ path: '/:id', auth: 'none', summary: 'Preview a post', handler: async ({ params, respond }) => { const post = await getPost(params.id); return respond.json({ title: post.title, excerpt: post.body.slice(0, 200) }); }, }), ], }), ],});
// App root: assemblyexport default defineApp({ port: 3000, packages: [blogPackage],});The blog package owns everything blog-related. The app root just assembles it. When you add an auth package or a search package, they compose alongside the blog package without touching its code.
When to use each layer
Section titled “When to use each layer”| You need… | Use |
|---|---|
| CRUD for a data model | Entity |
| Domain actions (publish, archive, complete) | Entity operations |
| A custom endpoint | Package route |
| A feature boundary | Package |
| Cross-package contracts | Package Contract (definePackageContract) |
| App-wide config (db, auth, security) | App root |
| Framework infra (auth, persistence, queues) | SlingshotPlugin |
Two authoring tiers
Section titled “Two authoring tiers”definePackage and SlingshotPlugin are parallel first-class authoring tiers, not
levels of a hierarchy. Pick by what you are building:
definePackage— your app’s features (blog, billing, chat). Declarative: entities, domain routes, capabilities. Most application code.SlingshotPlugin— framework-level plugins (auth, postgres, bullmq, custom middleware). Imperative lifecycle hooks for async bootstrap, registrar registrations, and conditional middleware. Almost every plugin you import is one.
Neither is a workaround for the other. See Composing an App for the full decision guide.
Learn each layer
Section titled “Learn each layer”Lower-level seams
Section titled “Lower-level seams”When generated package or entity behavior needs tweaking:
- Route overrides — customize one generated route
- Extra routes — add routes alongside generated ones
- Manual adapter wiring — bring your own storage
- Lower-level seams guide — decision guide for what to override