eGrantet is a municipal grants and subsidies management platform. Municipalities publish grant programmes with structured requirements; applicants discover grants, check eligibility, apply online with documents, and track every step; municipal staff review, assign evaluators, and make funding decisions — transparently and auditably.
Core principle: AI assists. Deterministic rules verify. Humans make final funding decisions.
AI may summarize applications, analyze documents, and flag inconsistencies. It never approves or rejects funding. Final decisions always belong to authorized humans.
- Production:
https://egrantet.vercel.app - Repository:
https://github.com/r1jadi/eGrantet - Languages: Albanian (Shqip), English, Serbian (Srpski, Latin)
- Features
- Tech stack
- Getting started
- Environment variables
- Database and migrations
- Project structure
- Authentication and authorization
- Security notes
- SEO, robots, and sitemap
- Cookies and legal pages
- Scripts
- Deployment
- Current status and known limitations
- Documentation map
- License
Applicant side:
- Public grant discovery with search, category, municipality, and sorting filters.
- Grant detail pages with funding, deadline, eligibility, and document requirements.
- Guided multi-step application wizard (applicant info, project plan, budget, documents, eligibility pre-check, review and submit).
- Draft lifecycle with correction-requested resubmission and appeal submission.
- Document upload (PDF, PNG, JPEG up to 10 MB) to private storage with ownership checks.
- Application dashboard, awarded-project portal with milestone evidence submission, notifications inbox, and notification/language preferences.
Municipality side:
- Grant lifecycle management (
DRAFT → PUBLISHED → CLOSED → ARCHIVED), ordered requirements, and evaluation criteria. - Application review queue with deterministic eligibility results, AI-assisted document verification panel, AI summaries, and decision letters (generated, human-reviewed, then sent).
- Evaluator assignment (including blind evaluation), milestone tracking, and audit history per application.
Platform administration:
- Platform-wide registry of municipalities, users and roles, grants, applications, evaluations, email deliveries, and the audit trail, protected by the server-side admin allowlist in addition to the
ADMINrole.
Cross-cutting:
- Deterministic eligibility engine independent of any AI provider.
- Server-side audit logging of important state changes.
- Transactional emails (status changes, decisions, milestones, evaluations) with per-user preferences and deduplication.
- Trilingual UI (sq / en / sr) with cookie-persisted language and light/dark theme.
- Cookie consent with accept / reject / customize, persisted on-device and changeable from the footer.
- Privacy Policy and Terms pages in all three languages, based on the real implementation.
- Content-specific skeleton loaders on all major routes, reduced-motion aware.
- Security headers (CSP, HSTS,
nosniff, referrer and permissions policies, frame denial) and best-effort in-memory rate limiting on auth, uploads, AI, and email actions.
| Layer | Technology |
|---|---|
| Framework | Next.js App Router (v16), React 19, TypeScript strict |
| Styling | Tailwind CSS v4, shadcn/ui-compatible components in src/components/ui |
| Backend | Next.js Server Actions (route handlers only for downloads/redirects) |
| Database | Supabase PostgreSQL, Auth, and private Storage |
| Validation | Zod (server and client), React Hook Form |
| AI | Provider-independent contracts + OpenAI-compatible gateway adapter |
| Resend (transactional only) | |
| Hosting | Vercel |
Architecture is a modular monolith: UI → Server Actions / Route Handlers → domain services → Supabase. See ARCHITECTURE.md. The UI never calls OpenAI or Resend directly and never touches a service-role client.
Prerequisites: Node.js 20+ and npm, plus a Supabase project (local via the Supabase CLI or hosted).
npm install
cp .env.example .env # then fill in the values, see below
npm run dev # http://localhost:3000Verify a change with the quality gate:
npm test # unit tests (integration tests are opt-in, see below)
npm run lint
npm run buildCopy .env.example to .env and set the values. Never commit .env (it is git-ignored) and never put secrets in NEXT_PUBLIC_* variables.
| Variable | Scope | Purpose |
|---|---|---|
NEXT_PUBLIC_SITE_URL |
Public | Canonical site URL, e.g. https://egrantet.vercel.app. Used for metadata, sitemap, and links. |
NEXT_PUBLIC_SUPABASE_URL |
Public | Supabase project URL. |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Public | Supabase anonymous key (safe for the browser; RLS still applies). |
SUPABASE_SERVICE_ROLE_KEY |
Server only | Service-role key. Used exclusively by server-side services after authorization checks. |
AI_API_KEY |
Server only | GroqCloud API key (free key at https://console.groq.com/keys). Never commit; set in .env.local + Vercel dashboard. |
AI_BASE_URL |
Server only | Provider base URL (defaults to GroqCloud https://api.groq.com/openai/v1). |
AI_MODEL |
Server only | Model id (defaults to Groq free-tier openai/gpt-oss-20b). Per-feature OPENAI_<FEATURE>_MODEL overrides take precedence. |
RESEND_API_KEY / RESEND_FROM_EMAIL |
Server only | Transactional email sending. |
ADMIN_EMAILS |
Server only | Comma-separated platform-admin allowlist. Empty = database role/membership model governs. |
RUN_SUPABASE_INTEGRATION_TESTS + SUPABASE_TEST_* |
Test only | Opt-in integration tests against a dedicated non-production project. |
NEXT_PUBLIC_APP_URL is still honored as a legacy fallback for the app URL, but new deployments should set NEXT_PUBLIC_SITE_URL.
Integration tests never run by default. To enable them you must explicitly set RUN_SUPABASE_INTEGRATION_TESTS=true with dedicated non-production credentials and fixture IDs.
- Schema changes live as ordered SQL files in
supabase/migrations/and follow the existing naming convention (YYYYMMDDHHMMSS_short_description.sql). - Apply migrations manually through the Supabase Dashboard SQL Editor. Do not run
supabase db pushagainst a database that may already contain manually applied migrations, and never reset or mutate the production database from a workstation. - Row Level Security stays enabled on all domain tables. Never disable RLS to make a feature work; fix the policy or the query scope instead.
- Refresh generated types after schema changes:
npm run db:types(requires a linked Supabase CLI session).
No migration is required for the current working tree beyond what is already in supabase/migrations/.
src/
├── app/ # App Router: public pages, route groups, API routes,
│ # loading/error states, robots.ts, sitemap.ts, icons
│ ├── (account)/ # notifications + settings (authenticated, any role)
│ ├── (admin)/ # platform administration (ADMIN + allowlist)
│ ├── (applicant)/ # applications, projects, business profile
│ ├── (evaluator)/ # evaluator workspace (assignment-scoped)
│ ├── (municipality)/ # municipality workspace (membership-scoped)
│ ├── (public)/ # home, grant directory, grant details
│ ├── api/documents/ # signed-URL download (auth-checked, 307)
│ ├── api/evidence/ # milestone-evidence download (auth-checked, 307)
│ ├── privacy/ terms/ # legal pages (sq/en/sr content)
│ └── robots.ts sitemap.ts manifest.ts icon.svg apple-icon.png
├── components/
│ ├── ui/ # generic design-system primitives (no domain logic)
│ ├── shared/ # shells, footer, skeletons, tables, states
│ ├── consent/ # cookie-consent banner + persistence logic + tests
│ └── legal/ # privacy/terms content per locale + tests
├── config/site.ts # public site metadata (name, URL)
├── lib/
│ ├── auth/ # server session, profile, roles, guards, redirects
│ ├── errors/ # AppError + safe user-facing messages
│ ├── i18n/ # locales (sq/en/sr), dictionaries, provider
│ ├── logging/ # server-side JSON logger (no PII in context)
│ ├── openai/ # server-only AI client + failure mapping
│ ├── resend/ # server-only email client
│ ├── security/ # best-effort in-memory rate limiting + tests
│ ├── supabase/ # browser / server / admin clients + middleware
│ └── validation/ # shared form validation helpers
├── modules/ # domain modules with public contracts via index.ts
│ ├── grants/ applications/ documents/ eligibility/ ai/ evaluations/
│ ├── grantee/ verification/ notifications/ audit/ users/
└── types/database.ts # generated Supabase types (refresh via db:types)
Module rules: eligibility never depends on AI; ai exposes provider-independent contracts; documents owns storage metadata while analysis lives in ai/verification; services use the service-role client only after server-side authentication and authorization.
Roles: APPLICANT, MUNICIPAL_EMPLOYEE, EVALUATOR, ADMIN.
- Authentication is verified server-side from the Supabase session on every sensitive operation. Role, ownership, municipality, and assignment values from the client are never trusted.
- Applicants access only their own applications and documents (IDOR-safe ownership checks on reads, updates, uploads, downloads, and deletions).
- Municipal employees are scoped to municipalities where they hold membership; evaluators only to explicitly assigned applications; admins additionally require the
ADMIN_EMAILSallowlist when configured. - The edge proxy refreshes sessions and applies coarse routing; it is not the authorization boundary — layouts, services, RLS policies, and storage checks each re-verify.
- Application downloads and milestone-evidence downloads resolve to short-lived signed URLs only after an authorization check; buckets stay private.
Implemented in this codebase:
- Security headers on all routes via
next.config.ts: Content Security Policy tailored to Next.js inline scripts and Google Fonts, HSTS,X-Content-Type-Options: nosniff, strict referrer policy, restrictive permissions policy, and frame denial (X-Frame-Options: DENY+frame-ancestors 'none'). API routes additionally sendX-Robots-Tag: noindex. - Server-side Zod validation at action/route boundaries, strict UUID checks on download IDs (replacing length-only checks), sanitized filenames and storage paths, upload type/size enforcement, and safe error mapping (no stack traces, database internals, or secrets reach users).
- Rate limiting on sign-in/sign-up/password flows, document uploads, AI verification/summary/letter generation, and decision-letter sending. It is intentionally dependency-free and per-instance, so on serverless hosting it is approximate — Supabase Auth's own provider-side limits apply in addition.
server-onlyboundaries with automated tests asserting no client component (inapp,components, or domain-module components) imports the service-role client..envis git-ignored;.env.examplecontains variable names and placeholders only.
Honest limits (see also Current status): no audit can promise zero vulnerabilities. npm audit still reports the braces chain (fix requires a breaking eslint-config-next downgrade, deliberately not applied). Review the Deployment notes before going live.
metadataBaseusesNEXT_PUBLIC_SITE_URL(production fallbackhttps://egrantet.vercel.app); root layout defines theeGrantettitle template, descriptions, Open Graph, Twitter, icons, theme colors, andWebSitestructured data.- Public pages (
/,/grants, published/grants/[id]viagenerateMetadata,/privacy,/terms) carry canonical URLs. Authenticated, admin, evaluator, and API routes arenoindexvia metadata orX-Robots-Tag— robots rules are a crawling hint only; data protection comes from authentication and authorization. app/robots.tsallows only canonical public paths and points to the production sitemap.app/sitemap.tslists static public pages plus published grant URLs (with a static-only fallback when the database is unreachable); drafts, applications, documents, and private IDs are never included.- After deploying, verify the live
/robots.txtand/sitemap.xmlon the production domain and register the property in Google Search Console (manual step, code-side prerequisites are in place).
- The consent banner offers Accept all / Reject optional / Customize with equal visual weight, persists the choice on-device, and never treats dismissal as consent. Essential session/security storage always runs; language/theme memory is a preference; no analytics or advertising scripts exist, and none load before consent.
- Reopen preferences anytime via the Cookie preferences link in the footer.
/privacyand/termsdescribe the real data practices and rules of this implementation in Albanian, English, and Serbian. They deliberately do not invent a legal entity, address, retention period, or official-government status — the deploying operator must confirm the data-controller identity, contact channel, and retention schedule (tracked below).
| Command | Purpose |
|---|---|
npm run dev |
Local development server |
npm run build |
Production build |
npm run start |
Serve a production build |
npm run lint |
ESLint (Next.js core-web-vitals) |
npm test |
Unit tests (tsx --test src/**/*.test.ts) |
npm run test:integration |
Opt-in Supabase integration tests (needs test credentials) |
npm run db:types |
Regenerate src/types/database.ts via the Supabase CLI |
- Deploy on Vercel from this repository (
vercel.jsonuses the Next.js framework preset). Set all environment variables in the project dashboard — never commit them. - AI on Vercel (required for verification/summaries/letters): set
AI_API_KEYto a GroqCloud key fromhttps://console.groq.com/keys.AI_BASE_URLandAI_MODELare optional — they default to GroqCloud andopenai/gpt-oss-20b. WithoutAI_API_KEY, AI features degrade gracefully (documents/summaries fall back to manual review; nothing is fabricated). - Apply any pending SQL in
supabase/migrations/manually via the Supabase Dashboard SQL Editor, in filename order, and verify RLS policies afterwards. - If Supabase runs on a custom domain, extend
connect-srcinnext.config.tsaccordingly. - After first deploy: open
/robots.txt,/sitemap.xml,/icon.svg,/apple-icon.png,/manifest.webmanifest,/privacy, and/termson the production domain; submit the sitemap in Google Search Console; confirm security headers on a public page.
What is implemented and verified (npm test, npm run lint, npm run build all pass; live-route smoke test against dummy backends passes):
- Full grant lifecycle, applications with drafts/corrections/appeals, document uploads and signed-URL downloads, deterministic eligibility, AI-assisted verification/summaries/decision letters (advisory only), evaluations with blind assignments, notifications with preferences, audit trail, milestones with evidence, trilingual UI, cookie consent, footer with legal pages, skeletons, SEO foundation, and the hardening listed above.
What the owner must still confirm or do manually (not code defects):
- Legal details: confirm the data-controller identity, privacy-contact channel, and retention schedule, then reflect them in
/privacyand the Terms contact section. - Database: apply migrations manually in the Supabase Dashboard; never
db pushblindly. No new migration is included in this change set. - Search: register the site in Google Search Console and submit
/sitemap.xml; indexing itself is Google's decision, not something a sitemap guarantees. - GitHub/Vercel: review the working tree, commit, push, and deploy manually — publishing was intentionally left untouched.
- Security: remaining
npm audithigh finding (bracesviaeslint-config-next) needs an upstream non-breaking fix; rate limiting is per-instance best-effort; the evidence-download endpoint currently serves municipality members (fail-closed for others) — applicant self-service evidence download is a possible follow-up, not a vulnerability.
Task tracking lives in TASKS.md; architecture decisions in ARCHITECTURE.md; agent workflow rules in AGENTS.md.
ISC — see package.json.