Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Modern SaaS client portal and CRM built with Next.js, TypeScript, Prisma and Supabase.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A multi-tenant CRM SaaS for freelancers and small agencies — every account is an Organization with its own team (staff members with roles), its own Clients/Projects/Tasks/Invoices, its own billing subscription, and an optional Client Portal its clients can log into directly. Built with Next.js App Router, Prisma, and Supabase.README 전문 보기 ↗A multi-tenant CRM SaaS for freelancers and small agencies — every account is an Organization with its own team (staff members with roles), its own Clients/Projects/Tasks/Invoices, its own billing subscription, and an optional Client Portal its clients can log into directly. Built with Next.js App Router, Prisma, and Supabase.
A complete, tested multi-tenant SaaS foundation: real multi-tenancy, real Paddle billing (buyer supplies their own account/credentials — see Billing), a Client Portal, attachments, comments, notifications, onboarding, analytics, search, and a Platform Admin console — all built on Server Components and Server Actions with no client-side data-fetching library.
Staff demo credentials (Owner):
[email protected] | |
| Password | DemoPassword123! |
A second staff account ([email protected], same password) is a plain Member of the same organization — sign in as either to see the same shared workspace, with the Team page showing both.
Client Portal demo credentials (a real, separate portal-only identity, invited into one of the demo clients):
[email protected] | |
| Password | DemoPassword123! |
All three accounts are seeded via npm run db:seed (prisma/seed.ts, see Database setup) into one already-connected demo organization — clients, projects, tasks, invoices, a couple of threaded comments (including an @-mention), and a couple of notifications, so every staff account lands directly on a populated dashboard, no extra setup step required. Attachments are deliberately not seeded: an Attachment row is a pointer to a real Supabase Storage object, and the seed script performs no Storage upload, so a fake row would just 404 — upload a real file through the app itself to see that flow.
These demo credentials are intentionally public — the seeded organization uses only fictional data (see prisma/seed.ts), but the account has full read/write access, so treat data in it as disposable and re-seedable, not as a stable fixture between sessions.
| Dashboard — live metrics, revenue trends, and status breakdowns | Clients — search, filter, sort, pagination |
| Projects | Tasks |
| Invoices | Client Portal — a client's own view of their invoices |
| Client detail — Client Portal access management | Project collaboration — threaded comments with @-mentions |
| Analytics — KPIs and trend charts derived entirely from the org's own data | Team — roles and membership |
| Billing settings — plan, usage, and Paddle plans (buyer-configurable; intentionally unconfigured in this demo) | Onboarding — first-login "Getting started" checklist |
Also: branded login · Client Portal — Projects
Organization; staff join it via a Membership with a role (OWNER / ADMIN / MEMBER), enforced server-side on every sensitive mutation (team management, billing, etc.), never just in the UI./portal) — a separate, thinner identity space (PortalUser, invited per-Client) where a client can log in to view their own projects and invoices — no access to staff data, no access to other clients./analytics, OWNER/ADMIN) — KPIs and trend charts derived entirely from the org's own data; no external analytics provider, no tracking./platform-admin) — a read-only, env-var-gated internal console for whoever operates the deployment (see Platform Admin) — separate from any tenant's own Organization.?q=&status=&sort=field:dir&page=), so state survives a refresh and is shareable as a link."use server" functions), colocated with the route that uses them (e.g. app/(dashboard)/clients/new/actions.ts). Forms use React's useActionState for pending/error state.middleware.ts refreshes that session on every request. Route protection happens in app/(dashboard)/layout.tsx, which redirects to /login if there's no session, and separately routes a Client Portal-only identity to /portal instead of the staff app.Client/Project/Task/Invoice/Attachment/Comment/...) is scoped by organizationId. The active organization is resolved server-side only — getCurrentUserOrganization()/getCurrentMembership() (src/lib/current-user.ts) verify the authenticated user actually holds a Membership in that organization before any query runs; an org id is never trusted from client input. Role-gated actions (billing, team management) additionally check membership.role.PortalUser is a structurally separate identity from staff User/Membership — it authenticates the same way (Supabase Auth) but has no Membership and no access to the staff app; it's scoped to exactly the Client it was invited for.PLATFORM_ADMIN_EMAILS), not a Role or Membership, since it's an operator-level concern that spans every organization (see Platform Admin).User.id/PortalUser.id are set to the Supabase Auth user's UUID directly (no separate mapping table). getOrCreateUser() upserts the Prisma User row on first authenticated request.| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Server Components, Server Actions) |
| Language | TypeScript |
| Styling | Tailwind CSS v4 |
| Database | PostgreSQL (via Supabase) |
| ORM | Prisma 7, with the @prisma/adapter-pg driver adapter |
| Auth | Supabase Auth (@supabase/ssr, cookie-based sessions) |
| File storage | Supabase Storage (attachments + org logos) |
| Resend | |
| Billing | Paddle (@paddle/paddle-node-sdk server-side, @paddle/paddle-js for checkout) — buyer-configurable, see Billing |
| Hosting target | Vercel |
| Seed runner | tsx (dev-only, runs prisma/seed.ts) |
No UI component library, no client-side state library, no toast library, no icon library — status badges, tables, dialogs, toasts, and icons are all small hand-built components under src/components/ui.
This is an illustrative subset — the app now spans many more domains (organizations/team, billing, notifications, attachments, comments, search, onboarding, platform admin) under the same conventions shown here. See src/ for the complete tree.
prisma/
schema.prisma # data model
migrations/ # applied migrations
seed.ts # demo data (creates real Supabase auth users too)
src/
app/
(auth)/ # /login, /signup — public
(dashboard)/ # protected staff route group, shares one layout
layout.tsx # auth check + active-org resolution + Sidebar/Header shell
dashboard/ # metrics + recent activity
clients/, projects/, tasks/, invoices/
page.tsx # list: search/filter/sort/pagination
query.ts # param parsing + Prisma where/orderBy builder
new/ # create form + action
[id]/edit/ # edit form + action
actions.ts # mutations
loading.tsx # route-level skeleton
team/ # invitations, roles
settings/ # company, domain, notifications, payment, billing
portal/ # Client Portal — separate identity, separate layout
(platform-admin)/ # operator-only console, env-var gated
api/
billing/webhook/ # Paddle webhook route
cron/ # notification delivery/cleanup
billing/checkout/ # Paddle.js checkout bridge page
components/
ui/ # Button, Input, Select, Table, StatusBadge, …
billing/, platform-admin/, list/, layout/, toast/, dashboard/, …
lib/
prisma.ts # Prisma client singleton (driver adapter)
current-user.ts # org/membership resolution — the tenancy boundary
billing/ # plans, entitlements, provider adapter, webhook logic
storage/ # Supabase Storage (attachments, logos)
notifications/, email/, onboarding/, search/, platform-admin/, rate-limit/, ...
types/index.ts # shared form-state types
middleware.ts # calls updateSession on every request
npm install
Copy the example file and fill in your own values (see Environment variables below):
cp .env.example .env
Follow Database setup below to apply the schema (and optionally seed demo data).
npm run dev
Visit http://localhost:3000.
If you're standing up your own deployment of this codebase, you supply your own infrastructure and billing configuration — nothing account-specific ships with this repository. At a glance, what you'll configure (details in .env.example and the docs linked from each row):
| Category | What you supply | Where |
|---|---|---|
| PostgreSQL / Supabase project | Your own Supabase project's connection strings | .env.example (DATABASE_URL/DIRECT_URL) |
| Supabase Auth | Nothing extra — works out of the box once the Supabase project + anon key are set | .env.example |
| Supabase Storage | Two buckets you create yourself in your Supabase project | Storage setup below |
| Email (Resend) | Your own Resend API key + sender domain (optional — app degrades gracefully without it) | .env.example |
| Vercel | Your own project/deployment | .env.example, Deployment |
CRON_SECRET | A random secret you generate | .env.example |
| Platform Admin | Your own email address(es), added to an env var | Platform Admin below |
| Billing (Paddle) | Your own Paddle account, API key, webhook secret, price IDs, client-side token | Billing below, docs/billing-provider-adapter.md |
This repository contains no Paddle account, no real API keys/secrets/price IDs, no KYC/KYB, and no payout/banking information — the current maintainer of this codebase has not created a Paddle account or entered any billing credentials. All of that is created and entered by whoever deploys their own instance, with zero application code changes required.
Full detail (including exactly where to find each value and what happens if an optional one is left unset) lives in .env.example — copy it to .env and fill in your own values; no real values are shown here.
| Category | Variables | Notes |
|---|---|---|
| Database | DATABASE_URL, DIRECT_URL | Supabase connection strings — pooled vs. direct, see below |
| Supabase | NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY | Service role key is server-only — used for seeding and Storage |
RESEND_API_KEY, INVITATION_FROM_EMAIL | Optional — invitations fall back to a copyable link if unset | |
| App | APP_BASE_URL, CRON_SECRET | Base URL for email links; secret required by the two cron routes |
| Platform branding/legal | PLATFORM_NAME, PLATFORM_TAGLINE, PLATFORM_LOGO_URL, PLATFORM_FAVICON_URL, PLATFORM_LEGAL_NAME, PLATFORM_LEGAL_ADDRESS, PLATFORM_SUPPORT_EMAIL, PLATFORM_JURISDICTION, PLATFORM_BILLING_EMAIL, PLATFORM_REPLY_TO_EMAIL | All optional, white-label the product's own identity/legal pages |
| Platform Admin | PLATFORM_ADMIN_EMAILS | Comma-separated allowlist — see Platform Admin |
| Billing (Paddle) | BILLING_PROVIDER, BILLING_ENVIRONMENT, BILLING_API_KEY, BILLING_WEBHOOK_SECRET, BILLING_STARTER_PRICE_ID, BILLING_PRO_PRICE_ID, NEXT_PUBLIC_BILLING_CLIENT_TOKEN | See Billing below |
DATABASE_URL uses the pgbouncer transaction pooler because it's what the deployed app talks to under serverless/edge concurrency. DIRECT_URL bypasses the pooler because schema migrations need a session-scoped connection — this split is configured in prisma.config.ts (CLI operations use DIRECT_URL) and src/lib/prisma.ts (the app's runtime client uses DATABASE_URL).
Two Supabase Storage buckets must exist in your own Supabase project before file uploads work — this app never creates them automatically (getStorageAdminClient() only connects to Storage, it never provisions a bucket):
| Bucket name | Visibility | Used for | Limits |
|---|---|---|---|
attachments | Private | Client/Project/Task attachments — accessed only via short-lived (60s) signed URLs, never a public link | 10 MB/file, 20 files per entity, allowlisted types (PDF, PNG/JPEG/WebP, TXT/CSV, DOC/DOCX/XLS/XLSX — no SVG/HTML/executables/archives) |
logos | Public | Organization logos — rendered directly via a permanent public URL | 2 MB/file, PNG/JPEG/WebP only |
To create them: in your Supabase project, go to Storage → New bucket, create attachments (leave it private) and logos (mark it public), using those exact names. No Storage RLS policies are required — both buckets are accessed exclusively through a server-side service-role client (SUPABASE_SERVICE_ROLE_KEY), which bypasses Storage RLS entirely; that key is never sent to the browser.
/platform-admin is a read-only console for whoever operates this deployment (not a tenant/customer concern) — organization/user lookups and a read-only view of platform-level configuration.
Access is controlled entirely by one env var, PLATFORM_ADMIN_EMAILS (src/lib/platform-admin/authorization.ts): a comma-separated, case-insensitive list of Supabase Auth email addresses. To get access yourself:
PLATFORM_ADMIN_EMAILS in your deployment's environment variables (e.g. PLATFORM_ADMIN_EMAILS="[email protected],[email protected]")./platform-admin while signed in with that email.Unset (the default) means nobody has access — an unauthenticated request to /platform-admin/* redirects to /login (the normal sign-in redirect every protected route uses), and a signed-in but non-matching email redirects silently to /dashboard, never revealing that the route exists. This grants access across every organization on the deployment, so keep the list to people who should actually operate the platform — never a tenant's own Role/Membership.
The full data model lives in prisma/schema.prisma — Organizations/Memberships/Invitations, Clients/Projects/Tasks/Invoices, the Client Portal (PortalUser), Attachments, Comments, Notifications, Billing (Subscription/WebhookEvent), and Onboarding — versioned as SQL migrations under prisma/migrations/.
Apply the schema to your database (non-interactive, safe for a fresh database or CI):
npx prisma migrate deploy
This is for a local/fresh database only. For the real Production database, never run a plain npx prisma migrate status/migrate deploy — prisma.config.ts only auto-loads a bare .env, never .env.production.db.local (the canonical local file for real Production credentials — see the Deployment section below), so a plain invocation can silently read stale credentials instead of failing loudly. Use npm run prisma:prod:status / npm run prisma:prod:deploy instead — see below.
(Optional) Seed demo data. Requires SUPABASE_SERVICE_ROLE_KEY to be set — the seed script (prisma/seed.ts) creates three real Supabase Auth accounts (two staff, one Client Portal contact — see Live demo above for credentials), a real Organization with both staff accounts as Members of it, and fills it with realistic clients, projects, tasks, invoices, comments, and notifications — already connected, so the very first login lands on a populated dashboard rather than an empty workspace:
npm run db:seed
This prints the demo login credentials (email + password) to the console.
(Optional) Browse the database with Prisma Studio:
npx prisma studio
During active development, use npx prisma migrate dev instead of migrate deploy when you change schema.prisma — it creates a new migration file and applies it in one step.
next build automatically. Make sure migrations have already been applied to the target database — the build does not run migrations for you. Never run a plain npx prisma migrate deploy against Production: it silently falls back to a bare .env's credentials if you forget to source .env.production.db.local yourself. Instead:
DATABASE_URL/DIRECT_URL in a local, gitignored .env.production.db.local (never committed, never any other tracked file). Not .env.production.local — despite the name, Next.js auto-loads that file locally during npm run build/next start, so it must never hold real credentials.npm run prisma:prod:status.npm run prisma:prod:deploy — this is a mutating, Production-authorized operation; run it deliberately, never as a routine step.scripts/prisma-production.mjs) refuse to run at all if .env.production.db.local is missing or incomplete, and never fall back to any other .env file.No further configuration is needed — there's no separate backend to deploy; Server Actions run as part of the Next.js deployment itself.
Two scheduled jobs, defined in vercel.json, retry failed notification
emails and clean up old read notifications — see
docs/notifications-architecture.md's cron section for the full retry/
cleanup policy. Both are gated by CRON_SECRET (src/lib/cron/auth.ts);
Vercel automatically sends it as Authorization: Bearer <CRON_SECRET> on
every scheduled invocation once the env var is set in Project Settings —
no extra configuration needed beyond adding that one variable.
Vercel plan note: this project assumes a Hobby plan, which caps
Cron Jobs at once per day per job. Both jobs below are scheduled daily
as a result — the delivery retry job's ideal cadence (every 15–30
minutes) needs a Pro plan or higher; if you're on Pro, tighten
vercel.json's schedule for /api/cron/notification-delivery accordingly.
organizationId (directly or via a relation), resolved server-side only (getCurrentUserOrganization()/getCurrentMembership()) — never trusted from client input. Delete/update operations use Prisma's updateMany/deleteMany with a compound { id, organizationId }-style where, since Prisma's unique-where update()/delete() can't express "this id, but only if it's mine" in one atomic query.notFound() response as a record that doesn't exist at all.<select> only ever lists the active organization's own clients/projects, but that's a UI convenience, not the security boundary — every action independently re-checks ownership before writing.membership.role on every call, not just in the UI.User/Membership), Client Portal (PortalUser), and Platform Admin (email allowlist) never share a resolution path — a Client Portal identity can never reach the staff app or another client's data; Platform Admin can never be granted via a tenant's own Role.DIRECT_URL); the deployed app uses the pooled connection (DATABASE_URL) — configured once in prisma.config.ts.anon/authenticated privileges on every table in the public schema — the app talks to Postgres exclusively through Prisma, not PostgREST.cookieOptions config, applied at every createServerClient(...) call site — flags any call site missing it.Three layers — unit (pure logic), integration (real Prisma against a real
PGlite-backed Postgres), and E2E (real Chromium against a real production
build) — plus a set of static security checks. See
docs/testing.md for the full architecture, how to run
each layer locally and in CI, what's deliberately not covered at the E2E
layer and why, and the TEST_MODE identity/Storage bypass E2E relies on
(and how it's verified to never affect a real deployment).
GitHub Actions (.github/workflows/) run on every pull request:
ci-fast.yml — prisma validate, type-check, lint, build, static security checks, unit tests.ci-integration.yml — integration tests, then a production build and the Playwright E2E suite.Both are intended to be green before merging a pull request.
| Command | Description |
|---|---|
npm run dev | Start the dev server |
npm run build | Production build |
npm run start | Run the production build |
npm run lint | ESLint |
npm run db:seed | Seed demo data (see Database setup) |
npx prisma migrate dev | Create/apply a migration in development (local database only) |
npm run prisma:prod:status | Check Production migration status — safe, read-only, requires .env.production.db.local |
npm run prisma:prod:deploy | Apply pending migrations to Production — mutating, requires .env.production.db.local and explicit authorization |
npx prisma studio | Browse the database |
npm test | Unit + integration tests |
npm run test:unit | Unit tests (Vitest) |
npm run test:integration | Integration tests against a real PGlite-backed Postgres |
npm run test:e2e | Playwright E2E — run npm run build first (see docs/testing.md) |
npm run security:check | Static security checks (scripts/security-checks/) |
Ideas for future iterations — not required for the product to work today:
A full, real, buyer-configurable Paddle billing integration — see docs/billing-architecture.md for the original design and docs/billing-provider-adapter.md for the adapter contract, exact env vars, and the sandbox → live checklist.
What's implemented and working today:
/settings/billing) — current plan, status, usage, Starter/Pro plan cards.@paddle/paddle-node-sdk server-side, @paddle/paddle-js for the checkout overlay) — real checkout, real Customer Portal, real signature-verified/idempotent webhook processing (POST /api/billing/webhook) that updates Subscription and notifies the org's OWNER.TEST_MODE (used only by this repo's own E2E suite, never in a real deployment) always takes priority over a real Paddle config if both happen to be present.TEST_MODE=1, exercising the entire pipeline end to end with zero network calls and zero payment data collected — this is test infrastructure, not something a real deployment ever uses.What you supply as the buyer: your own Paddle account (sandbox first, then live), API key, webhook secret, Starter/Pro price IDs, and Paddle client-side token — see the env var table above and docs/billing-provider-adapter.md's checklist for the exact steps, including Paddle's own domain-approval requirement. This repository contains none of that — no Paddle account has been created, and no real credentials, KYC/KYB, or payout information exist here; that's entirely your own setup with Paddle directly.
Deliberately not built (optional, not required for correctness): trial-ending email reminders and a billing-reconciliation cron job — both are defense-in-depth additions a real deployment can add later if desired; the webhook pipeline is already idempotent and correctly ordered without them.
Not yet validated against a real Paddle account: the checkout/webhook code has been built and tested against Paddle's own current, real API/documentation and a fully mocked SDK client, but has never been exercised against an actual Paddle sandbox delivery (since no Paddle account exists in this repository's own development). Recommended first step once you have a sandbox account: a real end-to-end checkout and webhook delivery test.
A dismissible "Getting started" checklist — see
docs/onboarding-architecture.md: a
minimal OrganizationOnboardingStep table (only for the two things that
can't be derived from real data — an explicit skip and an explicit
dismiss), a pure progress-computation engine that reads live
Client/Project/Task/Membership/PortalUser data, and skip/acknowledge/
finish Server Actions. A dismissible "Getting started" card renders at
the top of /dashboard (never a wizard, modal, or blocking overlay)
showing a progress bar and a per-step checklist with "Go to"/"Skip"
actions; it disappears once every substantive step is done or skipped,
or once explicitly dismissed. No settings entry point or resume
mechanism exists yet (deliberately deferred).
One step, "Review billing," points at the real /settings/billing page (see Billing above) — every staff role can view it, and it renders safely with no Paddle account configured at all, so reviewing billing never requires setting it up.
The Client Portal side is deliberately separate and much thinner: a
one-time "Welcome to your client portal" banner on /portal for a
newly-accepted PortalUser (eligibility inferred from createdAt, no
new table or column), with a non-persisted "Got it" dismiss and two
CTAs (View projects / View invoices). No checklist, no steps, no shared
state with the staff onboarding system above.
A staff-only Analytics dashboard (/analytics, OWNER/ADMIN only) — see
docs/analytics-architecture.md: KPI
cards (clients, projects, tasks, invoices, members, attachments,
completion rates), trend charts (client/project growth, task and
invoice activity, portal growth) built on Recharts, and a URL-driven
time range selector (today / 7 / 30 / 90 days / all time). A dedicated
Portal analytics section reuses the same chart components. Everything
is computed from this app's own first-party application records — no
external analytics provider, no analytics tracking cookie, and no
external telemetry pipeline. Portal Analytics adds only the
narrowly-scoped persistence documented in §12 (a portal user's most
recent login timestamp, and organization-scoped download-request
events with no per-user or per-document linkage). The Portal section
also includes two plain scalar metrics, "Recently active portal users"
and "Download-link requests" — current-selected-range counts with no
growth comparison or chart, deliberately scoped this way for privacy
reasons — see the architecture doc's own §12 for the full reasoning
and known limitations.
Portfolio project — no license specified.
npm run security:checknext.config.ts sets a Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, and a restrictive Permissions-Policy, applied to every route including API routes.src/lib/rate-limit), independent of any platform-level limiting.POST /api/billing/webhook verifies the provider signature before parsing anything, validates the claimed organization against the database before applying an event, and dedupes via a real database unique constraint — never a check-then-insert race.npm run security:check) run in CI on every PR, covering tenant isolation, the billing/webhook boundary, TEST_MODE isolation, cron auth, secret handling, and more (see scripts/security-checks/).