Lalabase
티켓·문서·근무시간·고객대화를 한 곳에 모아 KI로 검색, 소프트웨어 기업 고객 프로젝트용, 오픈소스로 자체 서버·독일 클라우드에 배포
GlideBook은 모바일 디테일러·예약 사업자를 위한 무료 예약 페이지로 고객이 패키지를 선택하고 시간 예약 후 보증금을 결제합니다
Free booking pages for mobile detailers and appointment businesses. Clients pick a package, choose a time in your area and pay a deposit.
비즈니스TypeScript · CSSGitHub @Apex-maker15온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Micro-SaaS booking for independent beauty pros (nail techs, lash artists, barbers) and mobile services (detailers, groomers). Clients pick a service and a slot, pay a deposit or the full price with Stripe, and the appointment lands on the provider's dashboard in real time.
Stack: Next.js 16 (App Router, React 19) · Tailwind CSS · Framer Motion · Lucide · Prisma 6 (engine-less, pg driver adapter) · PostgreSQL · NextAuth.js v5 · Stripe (Payment Element + webhooks) · Zustand · Server-Sent Events with Postgres LISTEN/NOTIFY.README 전문 보기 ↗Micro-SaaS booking for independent beauty pros (nail techs, lash artists, barbers) and mobile services (detailers, groomers). Clients pick a service and a slot, pay a deposit or the full price with Stripe, and the appointment lands on the provider's dashboard in real time.
Stack: Next.js 16 (App Router, React 19) · Tailwind CSS · Framer Motion · Lucide · Prisma 6 (engine-less, pg driver adapter) · PostgreSQL · NextAuth.js v5 · Stripe (Payment Element + webhooks) · Zustand · Server-Sent Events with Postgres LISTEN/NOTIFY.
Everything in this stack is open source or has a free tier. Stripe only takes a per-transaction fee when real money moves; test mode is free.
npm install
cp .env.example .env # defaults already point at the local PGlite database
npm run db:local # terminal 1 - WASM Postgres on 127.0.0.1:5433
npm run db:setup # terminal 2 - apply migration + seed two demo providers
npm run dev # http://localhost:3000
Demo logins (password password123):
| Provider | Booking page | |
|---|---|---|
| Polished by Apex (nails, GBP, 30% deposit) | [email protected] | /book/polished-by-apex |
| Shine Mobile Detailing (car) | [email protected] | /book/shine-mobile |
| Paws on Wheels Grooming (pet) | [email protected] | /book/paws-on-wheels |
Open the dashboard in one tab and the booking page in another - a new booking slides onto the board without a refresh.
.env.stripe listen --forward-to localhost:3000/api/webhooks/stripe
Paste the printed whsec_... into STRIPE_WEBHOOK_SECRET.4242 4242 4242 4242, any future expiry, any CVC. The webhook flips the booking to PAID and the dashboard glows.Without keys the app still runs end-to-end; the payment step shows a setup notice and the hold is kept.
| Piece | Free option | Notes |
|---|---|---|
| App | Vercel Hobby | npm run build runs prisma generate first. Set every variable from .env.example. |
| Database | Supabase free project | Use the direct connection string (session mode, port 5432). LISTEN/NOTIFY then works across serverless instances. |
| or Neon free project | Also fine. Realtime auto-detects that NOTIFY is unavailable and the dashboard still updates via SSE + 15 s polling. | |
| Payments | Stripe | No monthly fee. Add a webhook endpoint for https://<your-app>/api/webhooks/stripe with events payment_intent.succeeded, payment_intent.canceled, charge.refunded. |
Steps:
# once, against the production database
DATABASE_URL="postgresql://..." npx prisma migrate deploy
DATABASE_URL="postgresql://..." npx prisma db seed # optional demo data
Then import the repo into Vercel, paste the env vars, deploy. Set NEXT_PUBLIC_APP_URL to the Vercel URL.
prisma/schema.prisma)User - PROVIDER or CUSTOMER. Providers carry their business profile: slug, category (nails/beauty, hair, detailing, grooming, other), timezone, currency, locationMode (clients come to a studio vs. provider travels), studioAddress, depositPercent (100 = full prepay), slotIntervalMinutes, bufferMinutes, minNoticeMinutes, bookingHorizonDays. Customers are created as guests on their first booking.Service - name, duration, price in cents.Booking - PENDING → PAID | CONFIRMED | CANCELLED, UTC instants, price snapshot (amountCents) plus what was actually charged (depositCents), paymentIntentId.Availability - one row per weekday with { windows: [{start,end}], breaks: [{start,end}] } in the provider's local time.StripeEvent - processed webhook ids for idempotency./book/<slug> is the provider's own page, not a GlideBook form: cover and logo (uploaded from Your page in the dashboard, resized in the browser and stored inline as data URLs so no image host is needed), tagline, bio, Instagram, up to six work photos, a brand colour that re-tints the whole page (src/lib/color.ts derives light/dark "strong" shades), hours from availability, service areas, policies, and an embedded map for studio providers. Clients rate a booking from their manage link once the appointment has passed (POST /api/bookings/:id/review, one per booking); the average and latest reviews sit above the booking form.
Light and dark are CSS-variable themes switched by data-theme on <html> (src/components/theme/). The choice is saved in localStorage, defaults to the OS preference, and is applied by a beforeInteractive script so there is no flash. Tailwind's white/black are aliased to theme tints so existing bg-white/[0.06]-style classes work in both themes.
User.serviceAreas is free text shown on the booking page ("We come to you across Phoenix, Scottsdale..."). User.serviceAreaCodes is an optional comma-separated list of ZIP/postcode prefixes; when set, the booking form asks for the client's code and both the client (inline) and POST /api/bookings (OUT_OF_AREA) reject anything outside it. Matching is prefix-based and ignores case and spaces (src/lib/service-area.ts).
src/lib/slots.ts)Pure function. For a calendar day it walks each working window at slotIntervalMinutes, keeps a start t only if t + duration fits the window, does not intersect a break, respects the buffer around every existing booking, and is inside the notice/horizon range. All wall-clock maths goes through date-fns-tz, so DST is handled.
POST /api/bookings re-runs the same engine inside a transaction holding pg_advisory_xact_lock(hashtext(providerId)), so two customers can never take the same slot. Unpaid PENDING holds expire after BOOKING_HOLD_MINUTES; their PaymentIntents are cancelled lazily on the next read, no cron needed.
GlideBook is free for providers: PLATFORM_FEE_PERCENT defaults to 0 and the done-for-you setup has no fee (SETUP_FEE_CENTS = 0). Both can be switched on later without code changes. Deposits reach the provider one of three ways, chosen automatically per provider:
PAID. Refunds are between them.Optional revenue streams, both automatic once enabled:
/dashboard/payments (Stripe Express hosted onboarding, ~2 minutes on a phone). Deposits are created as destination charges on the platform account with transfer_data.destination = the provider and application_fee_amount = PLATFORM_FEE_PERCENT of the deposit (floor PLATFORM_FEE_MIN_CENTS). Stripe pays the provider out; the fee stays on the platform balance. Refunds reverse the transfer and the fee. Until a provider's account has charges_enabled, their page runs in pay-on-the-day mode: bookings confirm instantly with no card step, so the product is usable from minute one and the dashboard nudges them to connect.Enable Connect once in the Stripe dashboard (Connect → Get started → platform). Test mode needs no activation.
POST /api/checkout creates (or reuses) a PaymentIntent for the booking's snapshotted amount with an idempotency key, and returns the clientSecret.<PaymentElement> and confirms with redirect: "if_required". Redirect-based methods land on /book/return.POST /api/webhooks/stripe verifies the signature on the raw body, records the event id, then marks the booking PAID (or CANCELLED on cancel/refund) and publishes a realtime event./admin)The owner's email (DEFAULT_ADMIN_EMAILS in src/lib/admin.ts, extendable with ADMIN_EMAILS) sees an Owner link: platform revenue and deposits per currency, every provider with their Stripe state and activity, the latest bookings across the platform, and a per-provider workbench (/admin/providers/:id) with that provider's bookings and the same services / hours / settings editors, acting on their behalf via ?providerId=.
/dashboard/setup, /admin/setup)Providers can build their page themselves for free, or pay a one-off fee (GBP 5, SETUP_FEE_CENTS in src/lib/admin.ts) and paste their price list. The fee is a Stripe PaymentIntent with metadata.kind = "setup"; the webhook marks the SetupRequest paid. Emails listed in ADMIN_EMAILS see an Admin link with the queue: pick a request, edit that provider's services, hours and settings in place (the dashboard APIs accept ?providerId= for admins), then mark it done.
src/lib/email.ts sends over any SMTP provider (SMTP_URL, EMAIL_FROM); templates in src/emails/ are React Email components. Without SMTP configured, sends are logged and skipped..ics invite and the provider gets a new-booking alert. Cancellations notify both sides./api/cron/reminders runs daily via vercel.json (set CRON_SECRET) and emails clients whose appointment is 12-40 hours away./b/<bookingId>?t=<manageToken>, where the client can add the appointment to Google Calendar or cancel; cancelling outside cancelNoticeHours refunds the deposit automatically./book/<slug>/opengraph-image renders a branded preview card for links shared on Instagram, WhatsApp and iMessage.src/lib/realtime.ts, /api/events)The dashboard opens an EventSource. Server side, publish() emits to an in-process bus and, when the database provably delivers notifications (a probe is sent after LISTEN), through Postgres NOTIFY so every server instance sees every event. If the stream cannot be established the client falls back to 15 s polling, and it reconciles every 60 s and on tab focus regardless.
AnimatePresence mode="popLayout" with direction-aware slide + blur variants; the container animates layout so height changes never jump.layoutId so the active state morphs between elements.src/components/motion.ts), low stiffness / high damping.AnimatePresence mode="wait"; the Stripe iframe has a reserved min-height to avoid CLS.h1/h2); icons come only from src/components/icons.ts (Phosphor, duotone, hook-free SSR build so one import works in server and client components); no drop shadows, gradients, hover lifts or pill badges; corners are 3 to 8px (borderRadius scale in tailwind.config.ts); status colours are the theme-aware ok/warn/bad/info tokens rather than Tailwind's palette./terms and /privacy are linked from every footer; SUPPORT_EMAIL sets the contact address shown there.| Script | What it does |
|---|---|
npm run dev | Next.js dev server (Turbopack) |
npm run dev:webpack | Dev server with webpack - use on Windows-on-ARM / machines that block native binaries |
npm run build / start | Production build / serve |
npm run db:local | Zero-install local Postgres (PGlite) on port 5433 |
npm run db:setup | prisma migrate deploy + seed |
npm run db:migrate:dev | Create a new migration after editing the schema |
npm run db:studio | Prisma Studio |
npm run typecheck / lint | tsc --noEmit / ESLint |
engineType = "client" + @prisma/adapter-pg), which keeps serverless bundles small and avoids platform-specific binaries.src/proxy.ts (Next 16's middleware) protects /dashboard and bounces signed-in providers away from /login.