Employee Renewal System (ERS)
ERS is an internal renewal workflow system built with Next.js App Router + Prisma + Postgres.
Specs
docs/specs/ERS-v0.1-spec.mddocs/specs/ERS-v0.2-build-pack.mddocs/specs/ERS-v0.3-runnable-pack.md
Stack
- Next.js (App Router, TypeScript)
- Prisma + PostgreSQL
- Local filesystem uploads (
UPLOAD_BASE_DIR) - JWT cookie auth (email + password)
- Resend-backed durable email outbox
- PDF signature stamping with
pdf-lib
Environment
Set values in .env (and in /etc/ers-prod.env if you run via ers-prod.service):
DATABASE_URLAUTH_JWT_SECRETAUTH_COOKIE_NAMEUPLOAD_BASE_DIRUPLOAD_MAX_MBNEXT_PUBLIC_APP_VERSIONRESEND_API_KEYEMAIL_FROMNEXTAUTH_URL(public app base URL used in account email login links, e.g.https://review.r32a.com)OPENAI_API_KEY(optional, for smarter offer-letter extraction)OPENAI_MODEL(optional, defaultgpt-4.1-mini)INTERNAL_API_SECRETBOOTSTRAP_CEO_EMAIL(required fornpm run db:seed)BOOTSTRAP_CEO_NAME(required fornpm run db:seed)BOOTSTRAP_CEO_PASSWORD(required fornpm run db:seed)BOOTSTRAP_COMPANY_NAME(optional, used bynpm run db:seed)
Install
npm install
npm run prisma:generate
Database Setup
Set bootstrap values before running the seed:
export BOOTSTRAP_CEO_EMAIL="ceo@company.com"
export BOOTSTRAP_CEO_NAME="Chief Executive Officer"
export BOOTSTRAP_CEO_PASSWORD="replace-with-a-strong-password"
export BOOTSTRAP_COMPANY_NAME="Your Company Name"
Then run:
npm run prisma:migrate
npm run db:seed
Run
npm run dev
Open http://localhost:3000.
npm run db:seed bootstraps the initial CEO account and default settings. It also removes the legacy seeded @ers.local demo accounts if they are still present.
Core URLs
/login/hr/renewals/manager/requests/ceo/renewals/ceo/signature/admin/settings/admin/audit/admin/users/notifications
User Administration
- CEO can create users from
/admin/users. - CEO can activate/deactivate users from
/admin/users. - CEO can delete other users from
/admin/users. - Safeguards:
- CEO cannot delete their own account.
- Last remaining CEO account cannot be deleted.
- If a user has related records, delete is blocked and you should deactivate instead.
Offer Letter Autofill
- HR can upload an offer-letter PDF on
/hr/renewals/newand clickExtract & Prefill. - If
OPENAI_API_KEYis configured, extraction uses OpenAI structured parsing with heuristic fallback. - Uploading
OFFER_LETTERin a case detail also auto-maps extracted fields toRenewalCase. - On
/hr/renewals/new, HR can provideManager Email For Magic Link(and optional manager name) and choose to send the manager justification magic link immediately after case creation.
Email Dispatch
Outbound emails are queued in EmailOutbox. Dispatch endpoint:
POST /api/internal/email/dispatch- Header:
x-internal-secret: <INTERNAL_API_SECRET>
Recommended VPS cron (every minute):
* * * * * curl -X POST https://your-domain/api/internal/email/dispatch -H "x-internal-secret: your-secret"
Email-triggering flows:
- Creating a user from
/admin/usersqueues an account email to the new user (email, role, temporary password, and login URL) and immediately attempts delivery. Login URL in that email is built fromNEXTAUTH_URL(production:https://review.r32a.com/login). - Requesting manager justification queues email to the selected manager email and the linked internal manager account email (deduped), including both guest link and manager portal link, and immediately attempts delivery. Re-sending the request generates a new guest link without invalidating previously sent links; all outstanding guest links are revoked automatically after the first successful guest submission.
/api/internal/email/dispatchis still used for retries and any pending/failed outbox emails.
Notes
- HR/Manager cannot modify locked cases (
locked_atset by CEO decision). - CEO approval requires latest offer letter, performance snapshot, and manager justification.
- Signature stamping is enabled via
/api/renewals/:id/sign-pdfwith multi-placement payload. - Signing is allowed only when case status is
APPROVED. - After signing, case status becomes
SIGNED(closed/read-only) and HR users are notified via outbox email.