Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Мультитенантная CRM для логопедических центров
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Мультитенантный SaaS для логопедических центров Кыргызстана. Next.js 15 · TypeScript · Supabase (Postgres + Auth + RLS) · pnpm monorepo. Каталог logoped-crm/ — предыдущий одно-тенантный MVP, отдельный репозиторий. Новый код живёт в apps/ и packages/. Локальный стек (supabase start) требует Docker. Если его нет, проект уже подключён к облачной базе: Значения для apps/web/.env.local уже прописаны. В этом режиме работают pnpm dev и все проверки приложения; недоступен только pnpm db:test — pgTAP запускается лишь на локальном стеке.檢視完整 README ↗
Мультитенантный SaaS для логопедических центров Кыргызстана. Next.js 15 · TypeScript · Supabase (Postgres + Auth + RLS) · pnpm monorepo.
Каталог
logoped-crm/— предыдущий одно-тенантный MVP, отдельный репозиторий. Новый код живёт вapps/иpackages/.
Локальный стек (supabase start) требует Docker. Если его нет, проект уже
подключён к облачной базе:
| Проект | logocrm, регион ap-south-1 |
| Дашборд | https://supabase.com/dashboard/project/hiwstqrnxrlfuanvfggq |
| Миграции | 0001, 0002, 0003 применены |
| Тестовый вход | [email protected] / password123 |
Значения для apps/web/.env.local уже прописаны. В этом режиме работают
pnpm dev и все проверки приложения; недоступен только pnpm db:test —
pgTAP запускается лишь на локальном стеке.
Новые миграции в облако катятся так:
cd packages/db && supabase link --project-ref hiwstqrnxrlfuanvfggq && supabase db push
Тестовый пользователь с известным паролем живёт в базе, открытой в интернет. Перед первыми реальными данными его нужно удалить, а тариф проекта и политику паролей — пересмотреть.
corepack enable && corepack prepare pnpm@9 --activate)brew install supabase/tap/supabase)pnpm install
cd packages/db && supabase start
supabase start напечатает API URL и anon key. Перенесите их в конфиг веба:
cp apps/web/.env.example apps/web/.env.local
Заполните NEXT_PUBLIC_SUPABASE_URL и NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
(для старых проектов — NEXT_PUBLIC_SUPABASE_ANON_KEY, читаются оба), затем:
pnpm db:reset
pnpm dev
Приложение — http://127.0.0.1:3000, Supabase Studio — http://127.0.0.1:54323, почта локально (magic link) — http://127.0.0.1:54324.
Тестовый пользователь из seed: [email protected] / password123.
| Команда | Что делает |
|---|---|
pnpm dev | Next.js в режиме разработки |
pnpm build | production-сборка |
pnpm lint | ESLint |
pnpm typecheck | tsc --noEmit по всем пакетам |
pnpm test | Vitest в core и contracts |
pnpm db:start | поднять локальный Supabase |
pnpm db:stop | остановить его |
pnpm db:reset | прогнать миграции с нуля и применить seed |
pnpm db:test | pgTAP-тесты |
pnpm db:types | перегенерировать packages/db/src/database.types.ts |
pnpm db:link <ref> | связать репозиторий с облачным проектом |
pnpm db:push | накатить новые миграции в облако |
cd packages/db
supabase migration new add_students
Правьте созданный файл в supabase/migrations/, затем:
pnpm db:reset && pnpm db:test && pnpm db:types
После этого обязательно проверьте линтер Supabase — вкладка Advisors в
дашборде проекта. Он ловит то, чего не видят pgTAP-тесты: таблицы без RLS и
security definer-функции, открытые роли anon. Именно так были найдены
дыры, закрытые миграциями 0002 и 0003.
Миграции неизменяемы после мержа в main: ошибку исправляет следующая
миграция, а не редактирование старой.
Шаблон обязателен для всех таблиц с данными центра — подробности и объяснения в docs/Database.md.
create table public.students (
id uuid primary key default gen_random_uuid(),
center_id uuid not null default public.current_center()
references public.centers (id) on delete cascade,
full_name text not null,
custom_fields jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
created_by uuid default auth.uid(),
deleted_at timestamptz
);
create index students_center_idx on public.students (center_id) where deleted_at is null;
create trigger students_set_updated_at
before update on public.students
for each row execute function extensions.moddatetime(updated_at);
call public.apply_tenant_rls('students'); -- RLS: owner/admin своего центра
call public.apply_audit('students'); -- история изменений
Коротко о правилах: id — uuid; деньги — integer в тыйынах; статусы —
lookup-таблицы, не enum; удаление — только deleted_at; произвольные поля —
custom_fields jsonb.
После миграции добавьте pgTAP-тест «чужой центр не видит строку» и
перегенерируйте типы (pnpm db:types).
packages/contracts/src/events.ts:export const lessonCompletedSchema = z.object({
type: z.literal('lesson.completed'),
payload: z.object({
center_id: z.string().uuid(),
lesson_id: z.string().uuid(),
}),
})
export const appEventSchema = z.discriminatedUnion('type', [
centerCreatedSchema,
membershipCreatedSchema,
lessonCompletedSchema, // ← добавить
])
security definer-функции (RPC); из триггера —
emit_event_internal (0071); без сессии — emit_event_unchecked/_platform.
С 0075 клиентская роль emit_event не исполняет, и вызов из функции без
security definer упадёт permission denied вместе со всей записью:perform public.emit_event(
'lesson.completed',
jsonb_build_object('center_id', new.center_id, 'lesson_id', new.id)
);
events where processed_at is null, валидирует
строку через parseAppEvent() и проставляет processed_at.Имя события — сущность.действие в прошедшем времени. Доставка at-least-once:
обработчики обязаны быть идемпотентными (ADR-003).
Схема меняется только файлами в packages/db/supabase/migrations и только
через CI:
PR → CI (app + db с pgTAP + e2e) → merge в main → CI на main → джоб «Схема» → db push
Джоб «Схема» в ci.yml вызывает deploy-staging.yml с needs: [db, app].
Это не дисциплина, а зависимость между джобами: деплой физически не может
опередить ни pgTAP, ни Vitest-зеркала SQL-логики. Приёмка Playwright в гейт
деплоя не входит — она про интерфейс, а не про схему, — но обязательна для
мержа через required status checks на main.
Checkout в джобе деплоя берёт тот же коммит, что проверял pgTAP. Прежняя
связка через workflow_run брала верхушку main, и между зелёным прогоном и
деплоем мог проскочить непроверенный пуш.
Ручной запуск Deploy staging возможен только с main: с ветки он обошёл бы
pgTAP.
Prod (этап 8) автоматического деплоя не получит — туда руками и осознанно.
В Settings → Environments → staging нужен один секрет. Пока открыт
supabase/supabase#44210
— пулер отвергает пароль роли postgres после сброса, стабильно и во многих
регионах включая ap-south-1, — рабочий вариант только первый:
| Секрет | Где взять | Что даёт |
|---|---|---|
SUPABASE_ACCESS_TOKEN | Supabase → аватар → Account → Access Tokens → Generate | CLI получает временную роль через API, пароль базы не нужен. Токен аккаунтный, поэтому ref проекта зашит в workflow |
SUPABASE_DB_PASSWORD | Project Settings → Database → Reset | Строку собирает workflow. Доступ к одной базе, а не к аккаунту. Вернуться к нему, когда #44210 закроют |
SUPABASE_DB_URL | Connect → Session pooler | Запасной путь, только через пулер |
Приоритет в этом же порядке: если задан токен, пароль и строка не читаются. Держать стоит один секрет, а не все три.