1gr14/igrich/
  • Menu
    • Home
    • Start0
    • Support
    • Education
    • Blog
    • Author
  • Community
    • Discord
    • Telegram
  • Open Source
    • Point0
    • Route0
    • Error0
    • Flat
  • Account
    • Sign In
    • Sign Up
1gr14/igrich/
Building open-source software for the glory of the Lord Jesus Christ ☦️
With love for developers of all backgrounds around the world ❤️
Terms of ServicePrivacy PolicySergei Dmitriev 2026 😎

Start0

The SaaS boilerplate on Point0 — auth, admin panel, forms, emails, tests, and deploy, wired and working
Start0

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.

Get the Support Pack

What's inside

You start from a working app, not from a checklist

Point0

Pages, layouts, queries, mutations, and actions, typed end-to-end — the whole base follows the framework best practices

Auth

better-auth: email and password, Google sign-in, email verification, password reset, sessions, account linking

Admin panel

User management with filters and cursor pagination: view, edit, create, ban, impersonate

Database

PostgreSQL and Prisma 7: schema, migrations, typed JSON fields, and a seed script with realistic fake data

Background jobs

pg-boss in-process: queues and cron with retries, dedupe, and transaction-scoped enqueue — a heartbeat worker ships as the example

OpenAPI

Every query, mutation, and action lands in the generated spec. Scalar and Swagger UIs, behind basic auth

Forms

Zod schemas over react-hook-form: ready field components, validation, toasts and alerts on success and error

Emails

Resend and react-email: verification and password-reset templates as React components, with local preview

Error tracking

Typed errors on Error0 flow from server to client as JSON. Sentry on both sides captures every logged error

Logging

Structured LogTape logs: pretty in dev, JSON in production, emails and tokens auto-redacted

Metrics and alerts

Axiom on the server: every log stored and searchable in APL, a metric on every request, and alerts when the numbers move

Analytics

Mixpanel on both server and client: automatic page views, identity on sign-in, server events immune to ad-blockers

UI kit

React 19, Tailwind 4, and 40+ shadcn-style components: tables, dialogs, drawers, date pickers, dark mode

Example feature

Ideas — a working CRUD with infinite scroll: feed, create, edit, public author profiles. Copy the folder to start your own

Tests

Four setups wired: unit, DOM, integration, and e2e against a separate test database

Deploy

Dockerfile, docker-compose, and a Railway config. Health check, graceful shutdown, migrations on start

Code style

ESLint and Prettier tuned for the stack: typescript-eslint, React hooks, a11y, and Tailwind rules, one command to check it all

Editor setup

VS Code settings and recommended extensions in the repo

The stack

Bun end-to-end: runtime, package manager, scripts, tests
Bun LogoBunTypeScriptTypeScriptPoint0ReactReactTailwind CSSTailwind CSSshadcn/uishadcn/uiPostgreSQLPostgreSQLPrismaPrismaBetter Authbetter-authReact QueryTanStack QueryZodZodReact Hook FormReact Hook FormResendResendSentrySentryMixpanelMixpanelAxiomAxiomLogTapePlaywrightTesting LibraryTesting LibraryESLintESLintPrettierPrettierDockerDockerRailwayRailway

Getting started

One command brings the code. A few more bring it up
bun create start0@latest my-app

It 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 dev

Updates

Your app diverges from the base on day one. Updates respect that

Updating 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 update

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

Built for agents too

The repo teaches your coding agent how to work in it

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.

What's next

Planned for the base itself — each piece lands as an update

Billing

Stripe/DodoPayments/... subscriptions wired into the base, the same way auth is wired today

Tests in CI

A GitHub Actions workflow that runs the test setups on every push — the tests themselves are in the base today

Guide0

The future layer. None of it exists yet — here is the plan

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.

Get Start0

Included in the Support Pack — $10/month or $100/year

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.

Get the Support Pack

A real feature, in full

How development on Point0 feels inside Start0

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>
    )
  })

Get Start0

Included in the Support Pack — $10/month or $100/year

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.

Get the Support Pack

License

Build anything with it. Don't resell the base itself

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.

Need help?

Ask in the community Discord or Telegram, or email me at s@1gr14.dev