Build a contract generation system with customizable templates, variable substitution, e-signature, and PDF export
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are an expert full-stack engineer with deep knowledge of Malaysian business law document workflows. Build a complete Contract & Agreement Generator system for Malaysian SMEs.
## Project Context
Company Name: {{company_name}}
Company Address: {{company_address}}
Supported Contract Types: {{contract_types}}
Default Notice Period: {{default_notice_period}}
This system replaces the manual process of drafting contracts in Word documents. It supports employment agreements, service agreements, NDAs, tenancy agreements, and supplier contracts — all with Malaysian Employment Act 1955 compliance built in.
## Tech Stack
- Framework: Next.js 14 (App Router) + TypeScript (strict mode)
- UI: Tailwind CSS + shadcn/ui (new-york style, light mode, slate base color)
- Database: Supabase (PostgreSQL + RLS + Realtime)
- PDF Generation: @react-pdf/renderer
- Signature Capture: react-signature-canvas
- Template Engine: Handlebars.js (for {{variable}} placeholder substitution)
- File Storage: Supabase Storage (for generated PDFs)
- Form Validation: react-hook-form + Zod
- Deployment: Vercel
## Database Schema
Create the following complete Supabase PostgreSQL tables with all columns, indexes, and RLS policies:
**contract_templates**
- id (uuid, primary key, gen_random_uuid())
- name (text, not null) — e.g. "Standard Employment Agreement"
- type (text, not null) — enum: employment / service / nda / tenancy / supplier
- body (text, not null) — Handlebars template with {{variable}} placeholders
- clauses (jsonb, default '[]') — array of selected clause IDs
- version (integer, default 1)
- is_active (boolean, default true)
- created_by (uuid, references auth.users)
- created_at / updated_at (timestamptz, default now())
- Indexes: CREATE INDEX ON contract_templates(type), CREATE INDEX ON contract_templates(is_active)
**contracts**
- id (uuid, primary key)
- template_id (uuid, references contract_templates, not null)
- title (text, not null)
- variables_data (jsonb, not null) — key-value pairs of filled variables
- status (text, default 'draft') — enum: draft / sent / signed / active / expired / terminated
- generated_pdf_url (text) — Supabase Storage public URL
- sent_at / signed_at / expires_at (timestamptz)
- counterparty_name (text)
- counterparty_email (text)
- notes (text)
- created_by (uuid, references auth.users)
- created_at / updated_at (timestamptz)
- Indexes: CREATE INDEX ON contracts(template_id), contracts(status), contracts(expires_at), contracts(created_by)
**contract_variables**
- id (uuid, primary key)
- template_id (uuid, references contract_templates, on delete cascade)
- key (text, not null) — variable key used in Handlebars template, e.g. employee_name
- label_zh (text) — Chinese label shown in form
- label_en (text) — English label shown in form
- input_type (text) — enum: text / textarea / date / number / select
- default_value (text)
- options (jsonb) — array of strings for select input type
- is_required (boolean, default true)
- sort_order (integer, default 0)
- Index: CREATE INDEX ON contract_variables(template_id)
**signatures**
- id (uuid, primary key)
- contract_id (uuid, references contracts, on delete cascade)
- signer_name (text, not null)
- signer_email (text)
- signer_role (text) — e.g. employer / employee / witness
- signature_image (text) — Base64 PNG data URL of drawn signature
- signed_at (timestamptz, default now())
- ip_address (text)
- created_at (timestamptz)
- Index: CREATE INDEX ON signatures(contract_id)
**clause_library**
- id (uuid, primary key)
- category (text) — enum: employment / service / nda / tenancy / general
- title_zh (text) — Chinese clause title
- title_en (text) — English clause title
- body (text) — clause text, may include Handlebars placeholders
- is_mandatory (boolean, default false)
- legal_reference (text) — e.g. "Employment Act 1955, Section 12"
- sort_order (integer)
- created_at (timestamptz)
**template_versions**
- id (uuid, primary key)
- template_id (uuid, references contract_templates)
- version (integer)
- body_snapshot (text) — full template body at this version
- changed_by (uuid, references auth.users)
- changed_at (timestamptz, default now())
- Index: CREATE INDEX ON template_versions(template_id)
## RLS Policies
- contract_templates: SELECT for anon/authenticated where is_active = true; INSERT/UPDATE/DELETE for authenticated admin users only
- contracts: SELECT/INSERT/UPDATE for authenticated users matching created_by; anon can INSERT (for counterparty signing flow)
- signatures: INSERT for anon (counterparty signs via public link); SELECT for authenticated owner
- clause_library: SELECT for all; write for admin only
- Always wrap auth.uid() in (select auth.uid()) to prevent per-row evaluation
## Feature 1: Template Manager (/admin/contracts/templates)
Build a full template management interface:
- Template list page: table showing name, type, version, contract count, last updated, active status toggle
- Template editor: two-column layout
- Left: textarea/rich editor for template body with {{variable}} syntax highlighting via regex replace
- Right: variables panel — list all contract_variables for this template, add/edit/delete variables inline
- Clause library sidebar: drawer showing all clauses by category, click to insert at cursor position
- Version history panel: list all template_versions, click to preview diff, button to restore
- Auto-save: debounced 2s auto-save with visual indicator
- On save: insert row into template_versions before updating contract_templates
## Feature 2: Contract Generation Form (/contracts/new)
Split-view page (40% form / 60% live preview):
- Template selector at top: tabs or dropdown (Employment Agreement / Service Agreement / NDA / Tenancy Agreement / Supplier Contract)
- On template select: fetch contract_variables from Supabase, render dynamic form fields
- Form fields: text, textarea, date picker (shadcn/ui Popover + Calendar), number with RM prefix, select dropdown
- Validation: Zod schema generated dynamically from variable definitions
- Live preview (right panel): Handlebars.compile(template.body)(formValues) rendered as formatted HTML
- Company letterhead: company name, address, registration number, date
- Clause numbering: 1.0, 1.1, 1.2 hierarchy
- Variables highlighted in yellow until all fields filled
- Simulated A4 paper shadow effect
- Save as draft: POST to /api/v1/contracts — inserts row with status='draft'
- Generate PDF button: calls PDF generation server action, uploads to Supabase Storage
## Feature 3: PDF Generation
Create a Server Action generateContractPdf(contractId: string):
- Fetch contract + template + variables_data from Supabase
- Compile Handlebars template with variables_data
- Use @react-pdf/renderer to create PDF document:
- Page size: A4, margins: 2.54cm all sides
- Fonts: register Noto Serif for Latin and CJK characters
- Header: company name (bold, 14pt), address (10pt), horizontal rule
- Body: clause text (11pt, 1.5 line spacing), clause numbers bold
- Footer: page number, generation date, "Generated by {{company_name}}"
- Signature section: two columns with printed name lines and "Signature:" label
- Upload PDF buffer to Supabase Storage bucket 'contracts' with path: {created_by}/{contract_id}.pdf
- Update contracts.generated_pdf_url with public URL
- Return signed URL valid for 1 hour for immediate download
## Feature 4: E-Signature Flow (/contracts/[id]/sign)
Public signing page (no auth required for counterparty):
- Display full contract HTML (read-only, same rendering as preview)
- Signer info form: Full Name, Email, Role (Employer / Employee)
- Signature pad (react-signature-canvas): white background, black ink, 600x200px
- "Clear" button to reset canvas
- "Confirm Signature" button — disabled until canvas has content
- On submit:
1. Convert canvas to Base64 PNG: signatureCanvas.toDataURL('image/png')
2. Insert row into signatures table
3. Update contracts.status to 'signed', set signed_at = now()
4. Regenerate PDF with embedded signature image
5. Show success screen with download link
- Admin can send signing link via copy-to-clipboard button on contract detail page
## Feature 5: Contract Status Tracking
Implement status state machine:
- Draft → Sent: admin clicks "Send for Signature" button, sets sent_at
- Sent → Signed: counterparty completes signature flow
- Signed → Active: admin confirms, sets effective date
- Active → Expired: automated via Supabase pg_cron or check on page load comparing expires_at
- Any → Terminated: admin action with termination reason note
- Display status badge (shadcn/ui Badge): draft=slate, sent=blue, signed=green, active=emerald, expired=red, terminated=zinc
- Add status change log in contracts.notes field as append-only JSON array
## Feature 6: Bulk Contract Generation (/contracts/bulk)
CSV upload workflow:
- Template selector, then CSV upload dropzone (react-dropzone)
- CSV format: first row = variable keys matching selected template, subsequent rows = values
- Parse CSV client-side (papaparse library)
- Preview table: show first 5 rows with column mapping
- "Generate All" button: loop through rows, call generateContractPdf for each
- Progress bar showing N of total completed
- On completion: trigger ZIP download (jszip library) bundling all PDFs
- Insert all contracts to database with status='draft' and bulk_batch_id for grouping
## Feature 7: Dashboard (/contracts)
Main contracts page with:
- Summary cards row: Total Active Contracts, Awaiting Signature, Expiring in 30 Days, Drafts
- Status distribution: horizontal stacked bar using Tailwind widths (no chart library needed)
- Contracts table: columns = Title, Type, Counterparty, Status, Expires, Created — sortable headers
- Filters: status select, type select, date range picker, search by counterparty name
- Expiring soon panel: list of contracts where expires_at < now() + 30 days, with days remaining badge
- Template usage stats: bar list showing template name + usage count, sorted descending
## Malaysian Legal Content Requirements
The Employment Agreement template MUST include these clauses referencing Malaysian law:
- EPF / KWSP: Employer contributes 13% (or 12% if salary > RM 5,000), Employee contributes 11%
- SOCSO / PERKESO: Both parties contribute per SOCSO contribution schedule, capped at RM 5,000/month
- EIS (Employment Insurance System): 0.4% each from employer and employee
- Annual Leave: 8 days (< 2 years service), 12 days (2–5 years), 16 days (> 5 years) per Employment Act 1955 s.60E
- Sick Leave: 14 days (< 2 years), 18 days (2–5 years), 22 days (> 5 years) per s.60F
- Notice Period: {{default_notice_period}} (may be replaced by salary-in-lieu)
- Public Holidays: 11 gazetted public holidays per year per s.60D
- Overtime: 1.5x for weekday overtime, 2x for rest day, 3x for public holidays per s.60A
- Include stamp duty note: "This agreement is subject to stamp duty under the Stamp Act 1949"
- Governing law clause: "This agreement shall be governed by the laws of Malaysia"
## UI/UX Specifications
- Top navigation: dark background (#0F172A), white text, yellow (#FCD34D) accent on active item and primary CTA buttons
- Contract preview panel: white background, box-shadow for A4 paper effect, serif font for contract body text
- Form panel: clean white card, labels above inputs, helper text below, error messages in red-500
- Use shadcn/ui components: Card, Button, Input, Textarea, Select, Badge, Dialog, Table, Tabs, Sheet, Skeleton, Separator, Tooltip
- Loading states: Skeleton placeholders for contract list, spinner on PDF generation button
- Mobile responsive: stack form above preview on screens < 768px, hide preview until "Preview" tab clicked
- Use cn() from lib/utils for all conditional className merging
- Empty states: illustrated placeholder with CTA when no contracts exist yet
Generate the complete project with all pages, components, API routes, Supabase migration SQL, RLS policies, and seed data for the clause library. All code must be production-ready with no placeholder comments or TODO items.