With love for developers of all backgrounds around the world ❤️
Start0 is the base I build my own products on — this site included. It is a working app from the first minute: sign up, manage users in the admin panel, send emails, run the tests, deploy.
It is private and paid: the code comes with the Support Pack — $10/month or $100/year — and the subscription funds my open-source work on Point0 and the libraries.
Start0 is not part of Point0 — the framework itself is free and open source. If you want a bare Point0 project, Getting Started covers it: a create command and examples. Start0 is a production-ready implementation of Point0, with the extra tooling already wired.
I will cover Point0 and every integration around it in free articles and videos — that takes time. In Start0 the same things are already built and working: use them today instead of waiting, or assembling the pieces yourself.
A fair note: Point0 and the libraries are at an early stage, and bugs are possible. I react and fix fast — Start0 runs my own products, this site included, so fixes land quickly and reach you as updates. Thank you for the understanding.
bun create start0@latest my-appIt signs you in, checks your Support Pack, downloads the latest release, and runs the init script: your app's name everywhere and a fresh .env with a generated auth secret.
cd my-app
bun install
# fill DATABASE_URL and ADMIN_CREDENTIALS in .env
bun prisma:migrate
bun seed
bun devUpdating is not a merge. One command builds the diff of Start0 against itself — from the version you started on to the latest — and prints a prompt for your coding agent:
bunx 1gr14@latest updateThe agent applies what is relevant, adapts renamed pieces, and skips what you removed on purpose. Changelog entries with migration notes — and the version-marker bump — ride inside the diff. While your subscription is active, every new release is yours to pull.
AGENTS.md is the operating manual: where things go, what to read before a task, how to verify. Conventions live in docs/ and in JSDoc next to the code, linked with @related so an agent follows the graph instead of guessing. Point0 also ships a docs MCP server, so the agent reads the framework docs right in your editor.
The structure does half the work: src/modules for reusable machinery, src/features for product code, src/lib for small building blocks. A new feature starts as a copy of a sibling — for you and for the agent.
A Guide0 is one unit of content: a plain-language explanation — what it is, why it exists, and how to use it — plus proven, working code that embeds cleanly into your app, even a heavily customized one. The same guide works for you and for your coding agent. Not a generator — the guides follow the base structure, so they fit.
The guides will let you swap what the base decided for you and add what it does not carry: replace Stripe with DodoPayments, Lemon Squeezy, or another provider; deploy somewhere other than Railway — a managed platform or your own bare server; and plug in new capabilities, up to whole features.
The Support Pack is one subscription that funds the open-source work and gives rewards back: Start0 with updates, plus everything else listed on the pack page. Subscribe, then press Get Code — it offers three ways in: the bun create start0 CLI, a ZIP of the latest release, or an invite to the private GitHub repo.
Ideas is a feature that ships in the box — a small CRUD with infinite scroll, public author profiles, and create and edit forms. It is built the way you would build your own: a typed API of queries and mutations, pages that prefetch their data, forms wired to Zod schemas. Read it to feel what real development on Point0 looks like inside Start0 — then copy the folder and make it yours.
// src/features/idea/api.ts
import { paginateCursor } from '@/components/blocks/pagination'
import { AppError } from '@/lib/error'
import { root } from '@/lib/root'
import { zz } from '@/lib/schema'
import { authorizedOnlyPlugin } from '@/modules/auth/plugins'
import { prisma } from '@/modules/prisma'
import { ideaSelect, normalizeIdeaPayload } from '@/features/idea/server'
import { z } from 'zod'
export const ideaListQuery = root.lets
.infiniteQuery()
.input(
z.object({
...zz.shape.paginationCursor,
authorSn: zz.sn.optional(),
}),
)
.loader(async ({ input: { limit = 20, cursor, authorSn } }) => {
const items = await prisma.idea.findMany({
select: ideaSelect,
orderBy: { sn: 'desc' },
take: limit + 1,
where: {
...(authorSn ? { author: { sn: authorSn } } : {}),
...(cursor ? { sn: { lte: cursor } } : {}),
},
})
return paginateCursor({
items: items.map(normalizeIdeaPayload),
limit,
cursorKey: 'sn',
})
})
.infiniteQuery({
getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
initialPageParam: undefined,
pageParamFromInput: 'cursor',
})
export const ideaViewQuery = root.lets
.query()
.input(zz.object.sn)
.loader(async ({ input: { sn } }) => {
const idea = await prisma.idea.findUniqueOrThrow({
select: ideaSelect,
where: { sn },
})
return { idea: normalizeIdeaPayload(idea) }
})
.query()
export const ideaCreateMutationSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
export const ideaCreateMutation = root.lets
.mutation()
.use(authorizedOnlyPlugin)
.input(ideaCreateMutationSchema)
.loader(async ({ ctx, input: { title, content } }) => {
const idea = await prisma.idea.create({
select: ideaSelect,
data: { title, content, authorId: ctx.me.user.id },
})
return { idea: normalizeIdeaPayload(idea) }
})
.mutation()
export const ideaUpdateMutationSchema = z.object({
sn: zz.sn,
title: z.string().min(1),
content: z.string().min(1),
})
export const ideaUpdateMutation = root.lets
.mutation()
.use(authorizedOnlyPlugin)
.input(ideaUpdateMutationSchema)
.loader(async ({ ctx, input: { sn, title, content } }) => {
const existing = await prisma.idea.findUniqueOrThrow({
select: { authorId: true },
where: { sn },
})
if (existing.authorId !== ctx.me.user.id) {
throw new AppError('Only the author can edit this idea', {
code: 'FORBIDDEN',
})
}
const idea = await prisma.idea.update({
select: ideaSelect,
where: { sn },
data: { title, content },
})
return { idea: normalizeIdeaPayload(idea) }
})
.mutation()// src/features/idea/pages/list.tsx
import { InfiniteScroll } from '@/components/blocks/infinite-scroll'
import { Section } from '@/components/ui/section'
import { generalLayout } from '@/layouts/general'
import { IdeaCard } from '@/features/idea/components/idea-card'
import { ideaListQuery } from '@/features/idea/api'
import { mePlugin } from '@/modules/auth/plugins'
export const ideaListPage = generalLayout.lets
.page('/ideas')
.head('Ideas')
.use(mePlugin)
.page(({ props: { me } }) => {
const query = ideaListQuery.useInfiniteQuery()
return (
<Section h1="Ideas">
<InfiniteScroll
query={query}
loadMoreOnReachEnd
getItemKey={(idea) => idea.sn}
empty="No ideas yet. Be the first to share one."
itemClassName="border-b border-border last:border-b-0"
renderItem={(idea) => <IdeaCard idea={idea} me={me} />}
/>
</Section>
)
})// src/features/idea/pages/view.tsx
import { Button } from '@/components/ui/button'
import { Prose } from '@/components/ui/prose'
import { Section } from '@/components/ui/section'
import { routes } from '@/generated/point0/routes'
import { Link } from '@/lib/navigation'
import { zz } from '@/lib/schema'
import { formatDate } from '@/utils/date'
import { generalLayout } from '@/layouts/general'
import { ideaViewQuery } from '@/features/idea/api'
import { isMyIdea } from '@/features/idea/shared'
import { mePlugin } from '@/modules/auth/plugins'
export const ideaViewPage = generalLayout.lets
.page('/ideas/:sn')
.params(zz.object.sn)
.use(mePlugin)
.with(ideaViewQuery, ({ params }) => ({ sn: +params.sn }))
.head(({ params }) => `Idea #${params.sn}`)
.page(({ data: { idea }, props: { me } }) => {
return (
<Section
h1={idea.title}
action={
isMyIdea(idea, me) ? (
<Button
to={routes.ideaEdit({ sn: idea.sn })}
variant="outline-secondary"
>
Edit idea
</Button>
) : undefined
}
description={
<span className="flex flex-wrap items-center gap-2">
<Link
to={routes.userProfile({ sn: idea.author.sn })}
className="hover:text-foreground"
>
{idea.author.name}
</Link>
<span>·</span>
<span>{formatDate(idea.createdAt, 'date-time-nice')}</span>
{idea.updatedAt > idea.createdAt ? (
<span>· edited {formatDate(idea.updatedAt, 'date-nice')}</span>
) : null}
</span>
}
>
<Prose>
<p className="whitespace-pre-wrap">{idea.content}</p>
</Prose>
</Section>
)
})// src/features/idea/pages/new.tsx
import { Section } from '@/components/ui/section'
import { routes } from '@/generated/point0/routes'
import { navigate } from '@/lib/navigation'
import { generalLayout } from '@/layouts/general'
import { authorizedOnlyPlugin } from '@/modules/auth/plugins'
import { FButton } from '@/modules/form/core/button'
import { FFields, FFooter } from '@/modules/form/core/layout'
import { FForm } from '@/modules/form/core/provider'
import { FInput } from '@/modules/form/fields/input'
import { FTextarea } from '@/modules/form/fields/textarea'
import {
ideaCreateMutation,
ideaCreateMutationSchema,
ideaViewQuery,
ideaListQuery,
} from '@/features/idea/api'
export const ideaNewPage = generalLayout.lets
.page('/ideas/new')
.head('New Idea')
.use(authorizedOnlyPlugin)
.page(() => {
return (
<Section h1="New Idea">
<FForm
schema={ideaCreateMutationSchema}
defaultValues={{ title: '', content: '' }}
onSubmit={async ({ title, content }) => {
const { idea } = await ideaCreateMutation.fetch({ title, content })
void ideaListQuery.invalidateInfiniteQuery(true)
ideaViewQuery.setQueryData({ sn: idea.sn }, { idea })
return { idea }
}}
onSuccess={({ idea }) => {
void navigate('ideaView', { sn: idea.sn }, { replace: true })
}}
toastOnSuccess="Idea published"
size="sm"
>
<FFields>
<FInput
name="title"
label="Title"
placeholder="A short, catchy title"
inputSize="xl"
/>
<FTextarea
name="content"
label="Content"
placeholder="Share your idea…"
rows={10}
/>
</FFields>
<FFooter>
<FButton type="submit" size="2xl">
Publish
</FButton>
</FFooter>
</FForm>
</Section>
)
})// src/features/idea/pages/edit.tsx
import { Button } from '@/components/ui/button'
import { Section } from '@/components/ui/section'
import {
ideaListQuery,
ideaUpdateMutation,
ideaUpdateMutationSchema,
ideaViewQuery,
} from '@/features/idea/api'
import { routes } from '@/generated/point0/routes'
import { generalLayout } from '@/layouts/general'
import { navigate } from '@/lib/navigation'
import { zz } from '@/lib/schema'
import { FButton } from '@/modules/form/core/button'
import { FFields, FFooter } from '@/modules/form/core/layout'
import { FForm } from '@/modules/form/core/provider'
import { FInput } from '@/modules/form/fields/input'
import { FTextarea } from '@/modules/form/fields/textarea'
export const ideaEditPage = generalLayout.lets
.page('/ideas/:sn/edit')
.params(zz.object.sn)
.with(ideaViewQuery, ({ params }) => ({ sn: +params.sn }))
.head(({ params }) => `Edit Idea #${params.sn}`)
.page(({ data: { idea } }) => {
return (
<Section
h1="Edit Idea"
action={
<Button
to={routes.ideaView({ sn: idea.sn })}
variant="outline-secondary"
>
View idea
</Button>
}
>
<FForm
schema={ideaUpdateMutationSchema.pick({ title: true, content: true })}
defaultValues={{ title: idea.title, content: idea.content }}
onSubmit={async ({ title, content }) => {
const { idea: updated } = await ideaUpdateMutation.fetch({
sn: idea.sn,
title,
content,
})
ideaViewQuery.setQueryData({ sn: updated.sn }, { idea: updated })
void ideaListQuery.invalidateInfiniteQuery(true)
return { idea: updated }
}}
onSuccess={({ idea }) => {
void navigate('ideaView', { sn: idea.sn }, { replace: true })
}}
toastOnSuccess="Idea updated"
size="sm"
>
<FFields>
<FInput name="title" label="Title" inputSize="xl" />
<FTextarea name="content" label="Content" rows={10} />
</FFields>
<FFooter>
<FButton type="submit" size="2xl">
Save
</FButton>
</FFooter>
</FForm>
</Section>
)
})// src/features/idea/pages/my.tsx
import { InfiniteScroll } from '@/components/blocks/infinite-scroll'
import { Section } from '@/components/ui/section'
import { ideaListQuery } from '@/features/idea/api'
import { IdeaCard } from '@/features/idea/components/idea-card'
import { generalLayout } from '@/layouts/general'
import { authorizedOnlyPlugin } from '@/modules/auth/plugins'
export const myIdeaListPage = generalLayout.lets
.page('/my/ideas')
.head('My Ideas')
.use(authorizedOnlyPlugin)
.with(ideaListQuery, ({ props: { me } }) => ({ authorSn: me.user.sn }))
.page(({ queries: [query], props: { me } }) => {
return (
<Section h1="My Ideas">
<InfiniteScroll
query={query}
loadMoreOnReachEnd
getItemKey={(idea) => idea.sn}
empty="You haven't shared any ideas yet."
itemClassName="border-b border-border last:border-b-0"
renderItem={(idea) => <IdeaCard idea={idea} me={me} />}
/>
</Section>
)
})The Support Pack is one subscription that funds the open-source work and gives rewards back: Start0 with updates, plus everything else listed on the pack page. Subscribe, then press Get Code — it offers three ways in: the bun create start0 CLI, a ZIP of the latest release, or an invite to the private GitHub repo.
Start0 is source-available under the license in the Terms of Service: use the code to build your own products, commercial ones included. What you build is yours and stays yours after the subscription ends — what ends is access to the repo and future updates. You may not resell or redistribute the boilerplate itself, or turn it into a competing starter. The code ships as is, without warranties, and a short LICENSE.md travels in every copy.