Skip to content

Quick Start

You’re going to write one file: app.config.ts. That file declares your app. The CLI starts it. You’ll add a route, an entity, and auth in three steps.

Terminal window
bun add @lastshotlabs/slingshot hono zod

Create app.config.ts at the root of your project:

app.config.ts
import { defineApp, definePackage, domain, route } from '@lastshotlabs/slingshot';
const appPackage = definePackage({
name: 'app',
domains: [
domain({
name: 'health',
basePath: '/health',
routes: [
route.get({
path: '/',
summary: 'Health check',
handler: ({ respond }) => respond.json({ ok: true }),
}),
],
}),
],
});
export default defineApp({
port: 3000,
packages: [appPackage],
});

Boot it:

Terminal window
slingshot start

Test it:

Terminal window
curl http://localhost:3000/health
# → { "ok": true }

Your API docs are already live at http://localhost:3000/docs.

What just happened. slingshot start discovered app.config.ts, imported its default export, and booted a Hono server. defineApp is a typed identity helper — it’s literally config => config, but it gives you full autocomplete on the config object. definePackage created a feature module with one route. The route is typed, shows up in OpenAPI docs, and can be tested with curl.

Instead of writing five route handlers by hand, define one entity:

src/packages/app/task.entity.ts
import { defineEntity, field } from '@lastshotlabs/slingshot';
export const Task = defineEntity('Task', {
namespace: 'app',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
done: field.boolean({ default: false }),
},
});

Wire it into your config:

app.config.ts
import { defineApp, definePackage, entity } from '@lastshotlabs/slingshot';
import { Task } from './src/packages/app/task.entity';
const appPackage = definePackage({
name: 'app',
entities: [entity({ config: Task })],
});
export default defineApp({
port: 3000,
packages: [appPackage],
});

Five routes, generated from one definition:

Terminal window
# Create
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{ "title": "Ship v1" }'
# List
curl http://localhost:3000/tasks
# Get one
curl http://localhost:3000/tasks/<id>
# Update
curl -X PATCH http://localhost:3000/tasks/<id> \
-H "Content-Type: application/json" \
-d '{ "done": true }'
# Delete
curl -X DELETE http://localhost:3000/tasks/<id>

All five routes show up in OpenAPI docs with request/response schemas.

Terminal window
bun add @lastshotlabs/slingshot-auth

Update your entity to require login:

src/packages/app/task.entity.ts
import { defineEntity, field } from '@lastshotlabs/slingshot';
export const Task = defineEntity('Task', {
namespace: 'app',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
done: field.boolean({ default: false }),
},
routes: {
defaults: { auth: 'userAuth' }, // protect all CRUD routes
},
});

Then add the auth plugin to your config:

app.config.ts
import { defineApp, definePackage, entity } from '@lastshotlabs/slingshot';
import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';
import { Task } from './src/packages/app/task.entity';
const appPackage = definePackage({
name: 'app',
entities: [entity({ config: Task })],
});
export default defineApp({
port: 3000,
security: { signing: { secret: 'dev-secret-change-in-prod' } },
plugins: [createAuthPlugin({ auth: { roles: ['user'], defaultRole: 'user' } })],
packages: [appPackage],
});

Now your entity routes require a valid session, and you get auth routes for free:

Terminal window
# Register
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{ "email": "alice@example.com", "password": "supersecret" }'
# Login — returns a JWT
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "alice@example.com", "password": "supersecret" }'
# Use the JWT on protected routes
curl http://localhost:3000/tasks \
-H "Authorization: Bearer <token>"

Already have auth upstream or want to use your own? You don’t need slingshot-auth — see Bring Your Own Auth.

You have a running app with one entity and CRUD. The canonical next step is Composing an App — it walks you through how packages, custom routes, contracts, events, and plugins fit together as your app grows.

If you’d rather pick a single feature first, jump in here:

I want to…Read
Add domain operations (publish, archive, complete)Data and Entities
Send events across packages (welcome email, search sync)Events and the Event Bus
Push live updates to the browserRealtime
Add OAuth, MFA, or passkeysAuth
Control who can do whatPermissions
Run background jobsJobs and Orchestration
Use Postgres, SQLite, or MongoDBProduction Databases
Organize code as the app growsPackage-First Authoring
See a complete appExamples
Understand the framework modelAuthoring Model