A self-hosted proposal management tool built with Next.js, Supabase, and Lexical. Create and manage business proposals in single-tenant or multi-tenant mode.
- Single-tenant and multi-tenant modes via
NEXT_PUBLIC_TENANT_MODE - Organisations with role-based access (admin / member)
- Rich text editor powered by Lexical
- PDF export via headless Chromium
- AI assistant with configurable system prompts and organisation-level API keys
- Real-time updates via Supabase Realtime
- Drag-and-drop item reordering
- Public proposal sharing with optional token-based access
- Email sending for proposals and invitations (SMTP)
- MCP server endpoint for external AI tool access
- Internationalisation: English and German
- Framework: Next.js 15 (App Router)
- Language: TypeScript (strict)
- Database / Auth / Realtime: Supabase
- Editor: Lexical
- UI: Tailwind CSS + ShadCN UI (Radix primitives)
- State / Fetching: Zustand + SWR
- AI: Vercel AI SDK + Anthropic
- Drag & Drop: dnd-kit
- i18n: next-intl
- Node.js 18.17 or later
- pnpm (
npm install -g pnpm) - A Supabase project (cloud or self-hosted)
- Docker — only required for local Supabase development (not needed for cloud Supabase)
git clone <repository-url>
cd propositpnpm installcp .env.example .env.localFill in the values in .env.local:
| Variable | Description |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase publishable/anon key |
SUPABASE_SERVICE_ROLE_KEY |
Supabase service role key (server-side only) |
NEXT_PUBLIC_APP_URL |
Public URL of your app (e.g. http://localhost:3000) |
ENCRYPTION_KEY |
32-byte hex key — generate with openssl rand -hex 32 |
SMTP_HOST |
SMTP server host (optional — disables email if omitted) |
SMTP_PORT |
SMTP port |
SMTP_SECURE |
true for TLS, false for STARTTLS |
SMTP_USER |
SMTP username |
SMTP_PASS |
SMTP password or API key |
SMTP_FROM |
Sender address, e.g. "My App <hello@example.com>" |
NEXT_PUBLIC_TENANT_MODE |
single (default) or multi — see Tenant Mode |
INTERNAL_APP_URL |
Optional. URL Chromium uses for PDF export — see Deployment |
See .env.example for the full list including optional variables.
Apply all migrations to your Supabase project:
pnpm db:pushOr for local development with Docker:
pnpm db:start # start local Supabase
pnpm db:reset # apply all migrations to local DBSee context/supabase-local-dev.md for the full local development workflow.
pnpm devOpen http://localhost:3000 and sign up for an account.
pnpm dev # Start dev server (localhost:3000)
pnpm build # Production build
pnpm lint # ESLint check
pnpm lint:fix # ESLint auto-fix
pnpm type-check # TypeScript type checking
pnpm format # Format with Prettier
pnpm db:start # Start local Supabase (Docker required)
pnpm db:stop # Stop local Supabase
pnpm db:push # Push migrations to cloud database
pnpm db:reset # Re-apply all migrations on local DB
pnpm db:migrate # Create new migration fileapp/[locale]/
(auth)/ # Login, signup, password reset
(dashboard)/ # Protected routes (requires auth + organisation)
(public)/ # Public proposal view (/proposals/[id])
app/api/ # API routes (AI chat, PDF, organisations, MCP)
components/ # React components
ui/ # ShadCN UI base components
crud/ # Reusable DataTable and CrudForm
lib/ # Services, stores, utilities, types
messages/ # i18n translation files (en.json, de.json)
supabase/
migrations/ # All schema changes as SQL files
seed.sql # Sample data for local development
context/ # Developer reference docs
All schema changes are managed via Supabase CLI migrations in supabase/migrations/. Never edit the schema directly in the Supabase Studio.
# Create a new migration
pnpm db:migrate add_something
# Apply locally
pnpm db:reset
# Apply to production (before deploying code)
pnpm db:pushControlled by the NEXT_PUBLIC_TENANT_MODE environment variable.
| Mode | Behaviour |
|---|---|
single (default) |
One organisation. The first user registers normally and creates the organisation via the setup wizard. After that, the signup page is closed — further users must be invited by an admin. |
multi |
Open registration. Each user creates their own organisation. |
The app is a standard Next.js application and can be deployed to any platform that supports Node.js (Vercel, Railway, Fly.io, etc.).
The PDF export runs a headless Chromium in the same process as the app. The launcher auto-detects the environment:
- Node server / Docker / VPS / Railway / Fly.io — uses the full
puppeteerpackage with its bundled Chromium. Works out of the box. - Vercel / AWS Lambda — uses
puppeteer-core+@sparticuz/chromium, a stripped Chromium build sized for serverless functions.
On Vercel specifically:
- The PDF route declares
maxDuration = 60. The Hobby plan caps function duration at 10 seconds, which is usually too short to render a full proposal. Deploy on the Pro plan (or enable Fluid Compute) for reliable PDF export. - The function calls back into its own deployment to render the proposal page.
VERCEL_URLis used automatically. If you run the app behind a custom proxy or in a private network, setINTERNAL_APP_URLto the URL Chromium should reach.
If you prefer to run Chromium in a separate service (high volume, stricter isolation), see the discussion in issue #7.
Supported locales: en, de. Translation files are in messages/. To add a new language:
- Copy
messages/en.jsontomessages/<locale>.jsonand translate - Add the locale to the
localesarray ini18n.tsandmiddleware.ts
Pull requests are welcome. For larger changes, please open an issue first to discuss the approach.
- Fork the repository
- Create a feature branch:
git checkout -b feat/your-feature - Commit your changes:
git commit -m 'feat: add your feature' - Push and open a pull request
MIT — see LICENSE for details.
