Synaplan
開源 AI 平台,供企業使用:自行託管的 AI 代理、符合 GDPR 的聊天機器人小工具、RAG 知識庫、WhatsApp 與 Outlook AI、MCP、多模型路由。Docker、Kubernetes 或託管於 Germ
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Automated AI-powered contract analysis. Identify legal and business risks before you sign.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗
Automated AI-powered contract analysis. Identify legal and business risks before you sign.
ContractGuardian was born while testing Tanyra — a B2B meeting intelligence platform. During analysis of real contract negotiation meetings, a recurring gap emerged: contracts were being discussed without any structured risk review.
ContractGuardian is the validated prototype that solved that problem. It's open source because validating problems publicly is more credible than claiming solutions privately.
Home — Upload your contract
Step 1 — AI extracts contract metadata automatically
Step 2 — Validate metadata before running the full analysis
Step 3 — Structured report with findings per actor, severity classification and legal norm citations
| Layer | Technology |
|---|---|
| Framework | Next.js 15 App Router |
| Language | TypeScript (strict) |
| Styling | TailwindCSS v4 |
| ORM | Drizzle ORM |
| Database | Supabase (PostgreSQL) |
| AI | OpenAI GPT-4o-mini |
| PDF generation | pdf-lib |
| OCR | Tesseract.js |
| i18n | next-intl |
| Deploy | Railway |
ContractGuardian is built with Supabase and Railway as reference providers, but is not locked to them.
DATABASE_URL accordingly.The only hard dependency is a PostgreSQL database and an OpenAI API key.
# 1. Clone the repository
git clone https://github.com/Tigierre/contractguardian.git
cd contractguardian
# 2. Copy the environment template
cp .env.example .env.local
# 3. Fill in the required variables (see .env.example for guidance)
# - DATABASE_URL → PostgreSQL connection string
# - OPENAI_API_KEY → from platform.openai.com
# 4. Install dependencies
npm install
# 5. Push the database schema
npm run db:push
# 6. Start the dev server
npm run dev
Open http://localhost:3000.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes (runtime) | PostgreSQL connection string (any compatible provider) |
OPENAI_API_KEY | Yes (runtime) | OpenAI API key |
OPENAI_MAX_COMPLETION_TOKENS_ANALYSIS | No | Output-token cap per analysis call, reasoning included (default 16000) |
OPENAI_MAX_COMPLETION_TOKENS_PREANALYSIS | No | Output-token cap per pre-analysis call (default 6000) |
CG_ADMIN_GROUP | No | Group(s) granting admin rights (trash restore/purge), comma-separated |
AUTHENTIK_JWKS_URL | If CG_ADMIN_GROUP is set | JWKS used to verify the X-authentik-jwt token. Without it nobody is admin |
AUTHENTIK_JWT_ISSUER / AUTHENTIK_JWT_AUDIENCE | No | When set, the JWT must carry a matching iss / aud |
NEXT_PUBLIC_APP_URL | No | Base URL for the app (default: http://localhost:3000) |
NEXT_PUBLIC_SUPABASE_URL | No | Supabase project URL (only if using Supabase Auth — not used in v1) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | No | Supabase anon key (only if using Supabase Auth — not used in v1) |
BRAND_THEME | No | One of the bundled themes (see Theming). Empty = the default look |
BRAND_MODE | No | chiaro (light) or scuro (dark), only for themes that ship both |
BRAND_ACCENT | No | Brand colour for primary actions, #rrggbb |
.env.example documents every variable, including performance tuning and trash retention.
Neither DATABASE_URL nor OPENAI_API_KEY is needed at build time: both clients are created on first use.
The look of an instance is chosen with environment variables read at runtime, with no code changes and no rebuild:
BRAND_THEME=acciaio # nova · aurora · neon · sabbia · foresta · officina · acciaio
BRAND_ACCENT=#0f766e # optional: your brand colour on primary actions
[tema] …) and ignored.lib/temi/ (temi.css: one block of colour tokens per theme;
temi.ts: contrast helpers and voltoDaEnv()). They are shared files: update them by
copying newer versions over, not by editing them here.lib/tema-di-casa.css maps the Tailwind palette used by the
components (white, slate, red/rose, amber, green/emerald, blue/sky) onto
the theme tokens (surface, border, text, danger, warning, success, info). The dark:
variant is switched off and the light/dark toggle is not shown: the theme (and BRAND_MODE
for themes that have both modes) decides the mode.BRAND_ACCENT replaces --primary; the text on top of it (--on-primary) and its hover
are computed for contrast, so a light brand colour gets dark text. It also works without
a theme.acciaio → Roboto, officina → Barlow). Fonts come from npm
packages (@fontsource/*) and are served by the app itself, never from a CDN; the browser
downloads them only when the active theme uses them. Roboto is bundled; a theme whose font
package is not installed falls back to the system font.force-dynamic in app/[locale]/layout.tsx) so that the
variables are read when the container runs, not when the image is built.ContractGuardian has no login of its own. It is meant to run behind a reverse proxy
with forward-auth to Authentik (e.g. Traefik + an Authentik
outpost), which forwards the user identity in X-authentik-* headers. Two deployment
requirements follow:
X-authentik-* header sent by the client;In production, API requests without an identity are rejected with 403; every user
only sees their own contracts and analyses. Admin rights are never derived from the
plain-text groups header: they come from the signed JWT, verified against
AUTHENTIK_JWKS_URL.
ContractGuardian is optimised for Railway with a standalone Next.js build.
The railway.json and railpack.json configuration files are already included in the repository.
Steps:
For production, use the Session pooling Supabase connection string (port 5432) in DATABASE_URL.
You can build and run ContractGuardian as a Docker container on any platform.
docker build --build-arg SOURCE_COMMIT=$(git rev-parse HEAD) -t contractguardian .
SOURCE_COMMIT is baked into the image and reported by GET /api/health as
versione (short SHA), so you can always tell which code a container runs. Without
it the probe reports "versione": "sconosciuta".
docker run -p 3000:3000 \
-e DATABASE_URL="postgresql://user:password@host:5432/postgres" \
-e OPENAI_API_KEY="sk-proj-xxx" \
contractguardian
Or pass all variables from a file:
docker run -p 3000:3000 --env-file .env.local contractguardian
The container runs as a non-root user (nextjs), listens on port 3000 by default, and reads PORT from the environment if you need a different port.
GET /api/health answers 200 with {"status":"healthy","versione":"<sha>","database":"connected"},
or 503 when the database does not answer within 3 seconds. The image declares a
Docker HEALTHCHECK that calls it from inside the container (Node's built-in fetch,
no extra packages), so orchestrators see a container with an unreachable database as
unhealthy.
The container entrypoint applies any pending Drizzle migrations from
./db/migrations before starting the server. This means you do not need
to run npm run db:migrate manually after a deploy — point the container at
an empty (or up-to-date) PostgreSQL database and it converges. Applied
migrations are tracked in the __drizzle_migrations table, so re-runs are
idempotent.
If you prefer to manage migrations out of band, leave DATABASE_URL set but
remove the ./db/migrations folder from the image; the entrypoint will skip
the step.
The current implementation is built on Supabase (PostgreSQL) and deployed to Railway. The Dockerfile and standalone build are provider-agnostic — they work anywhere Docker runs.
It has been tested in production on:
Migrating to a different infrastructure (e.g. Azure, AWS, GCP) requires adapting the database connection string and, potentially, the storage/auth configuration. The codebase uses standard PostgreSQL via Drizzle ORM, so switching database providers is straightforward as long as the target is PostgreSQL-compatible.
Contributions to improve portability are welcome — open an issue or PR.
Two issues you may hit when building the Docker image on your own infrastructure (anything not Railway). Both are fixed in the current Dockerfile — documented here so future contributors don't re-introduce them.
pdf-parse and Next.js standalone tracing[email protected] ships a debug autotest at the top of its index.js that
reads ./test/data/05-versions-space.pdf the first time the module is
required when module.parent is falsy. Next.js' standalone output traces
required modules but does not copy the test fixture, so at runtime the
package crashes with:
Error: ENOENT: no such file or directory, open './test/data/05-versions-space.pdf'
The crash bubbles up as an HTML error page from /api/upload, and the
client sees Unexpected token '<', "<!DOCTYPE "... is not valid JSON.
Fix: import the internal file directly, bypassing the autotest:
// lib/pdf/extractor.ts
const pdfParse = require('pdf-parse/lib/pdf-parse.js'); // ✓
// const pdfParse = require('pdf-parse'); // ✗ crashes
next start (or the standalone server.js) does not touch your database
schema. On a fresh deploy this leaves the contracts table missing and the
API throws relation "contracts" does not exist.
Fix: the Dockerfile uses a docker-entrypoint.sh that runs
scripts/migrate.mjs (a small standalone script using
drizzle-orm/postgres-js/migrator) before exec node server.js. The
migrations folder and the script are copied into the runner stage
explicitly — the standalone tracer does not include .sql files on its own.
The script also copies drizzle-orm and postgres from the deps stage:
Next.js standalone bundles them into server.js but does not preserve
them as resolvable packages in node_modules, so a plain Node.js script
cannot import them otherwise.
A Prisma-style db/migrations/*.sql exclusion in .gitignore is a
foot-gun with Drizzle. Drizzle's meta/_journal.json references each
.sql file by name and applies them at runtime; without the files in
the repository, every fresh container starts with an empty database and
never converges. Keep the SQL files committed — they are not "generated
artifacts" in the Prisma sense, they are the authoritative description
of the schema.
reasoning_effort values changed for GPT-5.xOlder GPT-5 nano accepted reasoning_effort: 'minimal'. The current
gpt-5.4-nano (the default for pre-analysis) only accepts
'none' | 'low' | 'medium' | 'high' | 'xhigh', and returns a 400
invalid_request_error for 'minimal'. ContractGuardian uses 'low'
for pre-analysis (a light triage) and 'medium' for the deep analysis
pass.
If you pin a different model via OPENAI_MODEL_PREANALYSIS /
OPENAI_MODEL_ANALYSIS, verify which reasoning_effort values it
accepts — OpenAI changes the supported scale between model generations.
Contributions and ideas welcome — open an issue to discuss.
See CONTRIBUTING.md.
MIT — see LICENSE.
Built by Francesco Rossberger · [email protected] | Spawned from Tanyra validation — tanyra.com