Skip to content

Authoring Model

Every Slingshot app follows the same structure. Understanding it makes everything else click.

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 packages

App 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.

app.config.ts
// @skip-typecheck
import {
defineApp,
defineEntity,
definePackage,
domain,
entity,
field,
route,
} from '@lastshotlabs/slingshot';
// Entity: generated CRUD + operations
const 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 boundary
const 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: assembly
export 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.

You need…Use
CRUD for a data modelEntity
Domain actions (publish, archive, complete)Entity operations
A custom endpointPackage route
A feature boundaryPackage
Cross-package contractsPackage Contract (definePackageContract)
App-wide config (db, auth, security)App root
Framework infra (auth, persistence, queues)SlingshotPlugin

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.

When generated package or entity behavior needs tweaking: