Synaplan
Open-source AI platform for companies: self-hosted AI agents, GDPR-compliant chatbot widget, RAG knowledge base, WhatsApp and Outlook AI, MCP, multi-model routing. Docker, Kubernetes or hosted in Germ
Quello che hai detto, riletto da chi non era nella stanza — e ti fa dire: ecco quello che volevo dire.
Translation pending
BusinessTypeScript · CSSGitHub @TigierreOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Automated AI-powered contract analysis. Identify legal and business risks before you sign.
Taken from the repository README
Loading the full README.
Read the full 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