1gr14/игрич/
  • Меню
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
    • Agents Party
  • Аккаунт
    • Войти
    • Регистрация
1gr14/игрич/
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎
You have unsaved changes

Start0

SaaS-бойлерплейт на Point0 — авторизация, админка, формы, письма, тесты и деплой, уже связанные и рабочие
Start0

Start0 — это основа, на которой я делаю собственные продукты, включая этот сайт. Это работающее приложение с первой минуты: зарегистрируйтесь, управляйте пользователями в админке, отправляйте письма, запускайте тесты, деплойте.

Он закрытый и платный: код идёт вместе с Support Pack — $10 в месяц или $100 в год — и подписка финансирует мою опенсорс-работу над Point0 и библиотеками.

Start0 не является частью Point0 — сам фреймворк бесплатный и опенсорсный. Если нужен чистый проект на Point0, об этом есть Getting Started: команда создания и примеры. Start0 — это готовая к продакшену реализация Point0 с уже подключённой обвязкой.

Я буду разбирать Point0 и все интеграции вокруг него в бесплатных статьях и видео — на это нужно время. В Start0 всё это уже собрано и работает: пользуйтесь сегодня, вместо того чтобы ждать или собирать по кускам самому.

Честное замечание: Point0 и библиотеки на раннем этапе, баги возможны. Я реагирую и чиню быстро — на Start0 работают мои собственные продукты, включая этот сайт, так что фиксы прилетают быстро и доходят до вас обновлениями. Спасибо за понимание.

Оформить Support Pack

Что внутри

Вы начинаете с работающего приложения, а не со списка задач

Point0

Страницы, лэйауты, запросы, мутации и экшены, типизированные насквозь — вся основа следует лучшим практикам фреймворка

Auth

better-auth: почта и пароль, вход через Google, подтверждение почты, сброс пароля, сессии, привязка аккаунтов

Admin panel

Управление пользователями с фильтрами и курсорной пагинацией: просмотр, редактирование, создание, бан, вход под пользователем

Database

PostgreSQL и Prisma 7: схема, миграции, типизированные JSON-поля и seed-скрипт с правдоподобными данными

Background jobs

pg-boss внутри процесса: очереди и крон с ретраями, дедупликацией и постановкой задач внутри транзакции — в комплекте пример воркера-хартбита

OpenAPI

Каждый запрос, мутация и экшен попадают в генерируемую спеку. Интерфейсы Scalar и Swagger, закрытые basic auth

Forms

Схемы Zod поверх react-hook-form: готовые компоненты полей, валидация, тосты и алерты на успех и ошибку

Emails

Resend и react-email: шаблоны подтверждения почты и сброса пароля как React-компоненты, с локальным превью

Error tracking

Типизированные ошибки на Error0 доезжают с сервера на клиент как JSON. Sentry с обеих сторон ловит каждую залогированную ошибку

Logging

Структурные логи LogTape: читаемые в разработке, JSON в проде, почты и токены затираются автоматически

Metrics and alerts

Axiom на сервере: каждый лог сохранён и ищется через APL, метрика на каждый запрос и алерты, когда цифры поехали

Analytics

Mixpanel и на сервере, и на клиенте: автоматические просмотры страниц, идентификация при входе, серверные события, которым не мешают блокировщики

UI kit

React 19, Tailwind 4 и 40+ компонентов в стиле shadcn: таблицы, диалоги, шторки, выбор дат, тёмная тема

Example feature

Ideas — рабочий CRUD с бесконечным скроллом: лента, создание, редактирование, публичные профили авторов. Скопируйте папку, чтобы начать свою фичу

Tests

Настроены четыре набора: юнит, DOM, интеграционные и e2e против отдельной тестовой базы

Deploy

Dockerfile, docker-compose и конфиг Railway. Health check, мягкое выключение, миграции на старте

Code style

ESLint и Prettier, настроенные под стек: typescript-eslint, правила React-хуков, a11y и Tailwind — одна команда проверяет всё

Editor setup

Настройки VS Code и рекомендованные расширения прямо в репозитории

Стек

Bun от начала до конца: рантайм, пакетный менеджер, скрипты, тесты
Bun LogoBunTypeScriptTypeScriptPoint0ReactReactTailwind CSSTailwind CSSshadcn/uishadcn/uiPostgreSQLPostgreSQLPrismaPrismaBetter Authbetter-authReact QueryTanStack QueryZodZodReact Hook FormReact Hook FormResendResendSentrySentryMixpanelMixpanelAxiomAxiomLogTapePlaywrightTesting LibraryTesting LibraryESLintESLintPrettierPrettierDockerDockerRailwayRailway

С чего начать

Одна команда приносит код. Ещё пара — поднимает его
bun create start0@latest my-app

Она авторизует вас, проверяет вашу подписку Support Pack, скачивает последний релиз и запускает init-скрипт: имя вашего приложения везде и свежий .env со сгенерированным ключом авторизации.

cd my-app
bun install
# fill DATABASE_URL and ADMIN_CREDENTIALS in .env
bun prisma:migrate
bun seed
bun dev

Обновления

Ваше приложение расходится с основой в первый же день. Обновления это учитывают

Обновление — это не мерж. Одна команда строит дифф Start0 с самим собой — от версии, с которой вы начали, до последней — и печатает промпт для вашего кодинг-агента:

bunx 1gr14@latest update

Агент применяет то, что уместно, адаптирует переименованное и пропускает то, что вы удалили намеренно. Записи чейнджлога с заметками по миграции — и подъём маркера версии — едут внутри диффа. Пока подписка активна, каждый новый релиз ваш.

Сделано и для агентов

Репозиторий сам объясняет вашему кодинг-агенту, как в нём работать

AGENTS.md — это инструкция: что где лежит, что прочитать перед задачей, как проверить результат. Конвенции живут в docs/ и в JSDoc рядом с кодом, связанные через @related, чтобы агент шёл по графу, а не угадывал. Ещё Point0 поставляется с MCP-сервером документации, так что агент читает доки фреймворка прямо в вашем редакторе.

Половину работы делает структура: src/modules для переиспользуемой механики, src/features для продуктового кода, src/lib для маленьких кирпичиков. Новая фича начинается как копия соседней — и для вас, и для агента.

Что дальше

Запланировано для самой основы — каждая часть прилетит обновлением

Billing

Подписки Stripe/DodoPayments/… встроенные в основу так же, как сегодня встроена авторизация

Tests in CI

Воркфлоу GitHub Actions, который гоняет тесты на каждый пуш — сами тесты в основе есть уже сегодня

Guide0

Будущий слой. Пока ничего из этого нет — вот план

Guide0 — это одна единица контента: объяснение человеческим языком, что это, зачем и как этим пользоваться, плюс проверенный рабочий код, который аккуратно встраивается в ваше приложение, даже сильно переделанное. Один и тот же гайд работает и для вас, и для вашего кодинг-агента. Это не генератор — гайды следуют структуре основы, поэтому они подходят.

Гайды позволят заменить то, что основа решила за вас, и добавить то, чего в ней нет: заменить Stripe на DodoPayments, Lemon Squeezy или другого провайдера; задеплоить не на Railway, а на другую платформу или собственный сервер; и подключать новые возможности вплоть до целых фич.

Получить Start0

Входит в Support Pack — $10 в месяц или $100 в год

Support Pack — это одна подписка, которая финансирует опенсорс-работу и возвращает бонусы: Start0 с обновлениями плюс всё остальное, что перечислено на странице подписки. Подпишитесь и нажмите Get Code — там три пути: CLI bun create start0, ZIP последнего релиза или инвайт в приватный GitHub-репозиторий.

Оформить Support Pack

Настоящая фича целиком

Каково это — разрабатывать на Point0 внутри Start0

Ideas — фича, которая идёт в комплекте: небольшой CRUD с бесконечным скроллом, публичные профили авторов, формы создания и редактирования. Она сделана так же, как вы делали бы свою: типизированное API из запросов и мутаций, страницы, которые префетчат свои данные, формы, связанные со схемами Zod. Прочитайте её, чтобы почувствовать, как выглядит настоящая разработка на Point0 внутри Start0, — а потом скопируйте папку и сделайте её своей.

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

Получить Start0

Входит в Support Pack — $10 в месяц или $100 в год

Support Pack — это одна подписка, которая финансирует опенсорс-работу и возвращает бонусы: Start0 с обновлениями плюс всё остальное, что перечислено на странице подписки. Подпишитесь и нажмите Get Code — там три пути: CLI bun create start0, ZIP последнего релиза или инвайт в приватный GitHub-репозиторий.

Оформить Support Pack

Лицензия

Стройте на нём что угодно. Только не перепродавайте саму основу

Start0 распространяется как source-available по лицензии из Terms of Service: используйте код, чтобы строить свои продукты, в том числе коммерческие. То, что вы построили, — ваше и остаётся вашим после окончания подписки; заканчивается доступ к репозиторию и будущие обновления. Нельзя перепродавать или распространять сам бойлерплейт и делать из него конкурирующий стартер. Код поставляется как есть, без гарантий, и короткий LICENSE.md едет в каждой копии.

Нужна помощь?

Спросите в сообществе Discord или Telegram, либо напишите мне на s@1gr14.dev