Skip to content

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(...)
app.config.ts
src/
blog/
post.ts
post-operations.ts
package.ts
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
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
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
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],
});

Even this minimal app already gives you:

  • a real app root
  • package-owned routes
  • generated CRUD for posts
  • generated named operations like byAuthor, bySlug, and publish
  • 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”