Starter App
This is the canonical starter app used throughout the docs.
It is intentionally small, but it already uses the real core surfaces:
defineApp(...)packages: [...]definePackage(...)defineEntity(...)defineOperations(...)entity(...)
File layout
Section titled “File layout”app.config.tssrc/ blog/ post.ts post-operations.ts package.tssrc/blog/post.ts
Section titled “src/blog/post.ts”import { defineEntity, field, index } from '@lastshotlabs/slingshot';
export const Post = defineEntity('Post', { namespace: 'blog', fields: { id: field.string({ primary: true, default: 'uuid' }), slug: field.string(), authorId: field.string(), title: field.string(), body: field.string(), status: field.enum(['draft', 'published'] as const, { default: 'draft' }), createdAt: field.date({ default: 'now' }), updatedAt: field.date({ default: 'now', onUpdate: 'now' }), }, indexes: [index(['slug']), index(['authorId', 'createdAt'])], routes: { defaults: { auth: 'userAuth' }, list: { auth: 'none' }, get: { auth: 'none' }, create: { permission: { requires: 'blog:post.write' }, }, update: { permission: { requires: 'blog:post.write', ownerField: 'authorId', or: 'blog:post.moderate', }, }, },});src/blog/post-operations.ts
Section titled “src/blog/post-operations.ts”import { defineOperations, op } from '@lastshotlabs/slingshot';import { Post } from './post';
export const PostOperations = defineOperations(Post, { byAuthor: op.lookup({ fields: { authorId: 'param:authorId' }, returns: 'many', }), bySlug: op.lookup({ fields: { slug: 'param:slug' }, returns: 'one', }), publish: op.transition({ field: 'status', from: 'draft', to: 'published', match: { id: 'param:id' }, }),});src/blog/package.ts
Section titled “src/blog/package.ts”import { z } from 'zod';import { definePackage, domain, entity, route } from '@lastshotlabs/slingshot';import { Post } from './post';import { PostOperations } from './post-operations';
const previewBody = z.object({ markdown: z.string(),});
export const blogPackage = definePackage({ name: 'blog', mountPath: '/blog', entities: [ entity({ config: Post, operations: PostOperations, }), ], domains: [ domain({ name: 'preview', basePath: '/preview', routes: [ route.post({ path: '/', auth: 'userAuth', request: { body: previewBody }, responses: { 200: { description: 'Rendered preview' }, }, handler: ({ body, respond }) => respond.json({ html: `<article>${body.markdown}</article>`, }), }), ], }), ],});app.config.ts
Section titled “app.config.ts”import { defineApp } from '@lastshotlabs/slingshot';import { blogPackage } from './src/blog/package';
export default defineApp({ port: 3000, security: { signing: { secret: process.env.JWT_SECRET ?? 'dev-secret' }, }, packages: [blogPackage],});What this gives you
Section titled “What this gives you”Even this minimal app already gives you:
- a real app root
- package-owned routes
- generated CRUD for posts
- generated named operations like
byAuthor,bySlug, andpublish - generated docs at
/docs - a clean place to add events, SSE, WebSockets, permissions, and storage customization later
Where the rest of the docs continue this example
Section titled “Where the rest of the docs continue this example”- Package-First Authoring extends the
blogPackage - Entity System extends the
Postentity and its operations - Events and the Event Bus adds publication and subscriptions
- Realtime covers WebSockets and SSE delivery