Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# HackVillage environment template. Mirrors lib/env.ts (zod-validated server
# env) plus the handful of vars read from process.env directly (noted inline).
# Copy to .env for local dev. NEVER commit a filled-in .env (.gitignore blocks it).

# ── Core ────────────────────────────────────────────────────────────────
# PostgreSQL (Neon in prod, Docker locally)
DATABASE_URL=postgresql://user:password@localhost:5432/hackvillage
# Direct (non-pooled) connection for migrations — Neon-specific, optional locally
DIRECT_URL=postgresql://user:password@localhost:5432/hackvillage
DATABASE_URL=postgresql://user:***@localhost:5432/hackvillage

# Auth.js v5 (Phase 1)
NEXTAUTH_SECRET= # generate: openssl rand -base64 32
Expand All @@ -20,25 +22,52 @@ BREVO_API_KEY=
EMAIL_FROM="HackVillage <info@hackvillage.xyz>"

# ── Paystack (test keys are sufficient for local development) ────────────
# Absent key switches payments to the dev simulation port.
PAYSTACK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY=pk_test_...
PAYSTACK_WEBHOOK_SECRET=
# Explicit opt-in to run the SIMULATION payment port in production — the
# pre-go-live state. Without this a keyless production boot fails closed.
# Set to "true" ONLY until real Paystack keys go in.
PAYSTACK_ALLOW_SIMULATION_IN_PROD=

# ── Chain — Polygon (Amoy testnet first; mainnet only after audit) ──────
# Absent config switches attestations to simulation (marked on /trust).
SMART_CONTRACT_ADDRESS=0x... # PrizeVaultFactory (ADR-007)
CHAIN_ID=80002
RPC_URL=https://rpc.amoy.polygonscan.com
ATTESTER_PRIVATE_KEY=0x... # platform signer — env-only, NEVER in the repo
POLYGONSCAN_URL=https://amoy.polygonscan.com

# ── Storage ────────────────────────────────────────────────────────────
# ── Storage (read from process.env by lib/ports/storage.ts) ──────────────
# R2/S3-compatible in production; without these, media saves to public/uploads.
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=hackvillage-media
# Public URL prefix for uploaded media. Defaults to
# https://pub-<R2_ACCOUNT_ID>.r2.dev/<R2_BUCKET> — set when using a custom domain.
R2_PUBLIC_BASE=

# ── Ops (defaults shown; values are enforced by lib/env.ts) ──────────────
# ── Error tracking (Sentry — all optional; unset = complete no-op) ───────
# Server + edge runtimes (instrumentation.ts, sentry.*.config.ts)
SENTRY_DSN=
# Browser runtime (instrumentation-client.ts, app/error.tsx) — public by design
NEXT_PUBLIC_SENTRY_DSN=
# Source-map upload at build time (next.config.ts withSentryConfig)
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_AUTH_TOKEN=

# ── SEO ops ─────────────────────────────────────────────────────────────
# scripts/ping-indexnow.ts: site root submitted to IndexNow (defaults to
# https://www.hackvillage.xyz; override for a future domain)
INDEXNOW_SITE=

# ── Ops (defaults shown; values are enforced by lib/env.ts) ──────────────
PAYOUT_MANUAL_REVIEW_THRESHOLD_KES=500000
PLATFORM_FEE_BPS=500 # 5% organizer-funded fee at deposit (ADR-012)
MIN_PRIZE_POOL_KES=10000 # minimum escrowed pool (Decision D3)

# ── Removed dead vars (2026-09-27 audit; grep confirms zero code refs) ────
# DIRECT_URL — Prisma datasource uses DATABASE_URL only (no directUrl).
# PAYSTACK_WEBHOOK_SECRET — webhook HMAC uses PAYSTACK_SECRET_KEY.
# CHAIN_ID — the chain port hardcodes Amoy; RPC_URL selects the network.
# NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY — checkout is server-initialized.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ jobs:
- name: Apply database migrations
run: pnpm run db:deploy

- name: Unit + integration tests
run: pnpm test
- name: Unit + integration tests (coverage gate on services/escrow + services/payout)
run: pnpm run test:coverage

- name: Build
run: pnpm run build
Expand Down
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ pnpm-debug.log*
*.log

# local env files — never commit credentials (CONTRIBUTING.md)
# .env.* covers .env.development/.env.test/.env.production etc.; the only
# committable env file is the documented template.
.env
.env.local
.env.*.local
.env.*
!.env.example

# vercel
.vercel
Expand All @@ -46,6 +49,3 @@ next-env.d.ts

# claude-flow local tooling data
.claude-flow/

# production env (pulled from Vercel — contains real credentials)
.env.production
31 changes: 20 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ Large features (new escrow flows, DAO mechanics, cross-border payment rails) sho
| Requirement | Minimum Version |
|---|---|
| Node.js | 22.13+ |
| pnpm | 9.x |
| pnpm | 11.x (`corepack enable` pins the version from `package.json`) |
| PostgreSQL | 15.x |
| Git | 2.x |

You will also need:

- A **Paystack** account — test keys are sufficient for local development.
- An RPC endpoint for the target chain (Polygon Mumbai testnet or Solana Devnet) for escrow contract work.
- An RPC endpoint for the target chain (Polygon Amoy testnet) for escrow contract work.

### Installation

Expand All @@ -112,12 +112,16 @@ cp .env.example .env.local
| Variable | Description |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string |
| `PAYSTACK_SECRET_KEY` | Paystack secret key (`sk_test_...` for local) |
| `NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY` | Paystack public key (`pk_test_...` for local) |
| `SMART_CONTRACT_ADDRESS` | Deployed escrow contract address |
| `RPC_URL` | JSON-RPC endpoint for the target chain |
| `NEXTAUTH_SECRET` | Random secret for session signing |
| `NEXTAUTH_URL` | Base URL (`http://localhost:3000` locally) |
| `NEXT_PUBLIC_APP_URL` | Public base URL used in links/metadata |
| `PAYSTACK_SECRET_KEY` | Paystack secret key (`sk_test_...` for local; absent = simulation) |
| `SMART_CONTRACT_ADDRESS` | Deployed escrow contract address |
| `RPC_URL` | JSON-RPC endpoint for the target chain |
| `ATTESTER_PRIVATE_KEY` | Platform signer for chain attestations (env-only) |

The full, annotated variable list lives in `.env.example` and is validated by
`lib/env.ts`.

Never commit `.env.local` or any file containing real credentials.

Expand Down Expand Up @@ -146,8 +150,12 @@ HackVillage/
├── components/ # Shared React components
├── lib/ # Shared utilities, DB client, API helpers
├── services/
│ ├── escrow/ # Escrow microservice — Paystack + smart contract calls
│ └── payout/ # Split disbursement logic
│ ├── escrow/ # Deposits, vault, webhook, attestations, reconcile
│ ├── payout/ # Split disbursement engine, tranches, milestones
│ ├── judging/ # Rubrics, scoring, feedback gate, results
│ ├── media/ # 48h deadline, trust events, notifications
│ ├── legacy/ # 3-month check-ins, disputes
│ └── pow/ # Endorsements, portfolio, hiring intros
├── contracts/ # Smart contract source and ABI
├── db/
│ ├── migrations/ # PostgreSQL migrations
Expand Down Expand Up @@ -292,9 +300,10 @@ CI will fail on lint or type errors. Do not disable rules with `eslint-disable`
Tests live in the `tests/` directory, mirroring the source structure.

```bash
pnpm test # Run all tests
pnpm run test:unit # Unit tests only
pnpm run test:e2e # End-to-end tests (requires running dev server)
pnpm test # Run all tests
pnpm run test:unit # Unit tests only
pnpm run test:integration # Integration tests (live Postgres — see tests/integration)
pnpm run test:coverage # Suite + coverage gate on services/escrow + services/payout
```

### Requirements
Expand Down
45 changes: 30 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ HackVillage operates as a three-phase engine:
```
┌───────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ Next.js 14 (SEO-optimized, │
│ Next.js 15 (SEO-optimized, │
│ server components, App Router) │
└────────────────────────┬──────────────────────────────┘
│
Expand All @@ -147,19 +147,22 @@ HackVillage operates as a three-phase engine:
└──────────┬─────────────────────────┬──────────────────┘
│ │
┌──────────▼──────────┐ ┌──────────▼──────────────────┐
│ PostgreSQL DB │ │ Escrow Microservice │
│ PostgreSQL DB │ │ Escrow service (in-repo) │
│ Structured Proof │ │ Paystack + Smart Contract │
│ of Work tracking │ │ (Polygon / Solana layer) │
│ of Work tracking │ │ (Polygon Amoy) │
└─────────────────────┘ └─────────────────────────────┘
```

> The separate escrow microservice and Paystack Split API were both evaluated
> and rejected — see ADR-001 (modular monolith) and ADR-006 (direct transfers).

| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Next.js 14 | SEO-optimized project discovery |
| Frontend | Next.js 15 | SEO-optimized project discovery |
| Backend | Node.js / TypeScript | High-concurrency event-day API |
| Database | PostgreSQL | Structured Proof of Work records |
| Payments | Paystack | Fiat deposits, M-Pesa & bank payouts |
| Ledger | Smart Contract (Polygon/Solana) | Transparent, tamper-proof transaction log |
| Ledger | Smart Contract (Polygon Amoy) | Transparent, tamper-proof transaction log |
| Open Source Core | GitHub | Judging logic & escrow mechanics are public |

---
Expand Down Expand Up @@ -229,12 +232,13 @@ Paystack API ──── Final transfer ──── Public Ledger entry record

| Phase | Feature | Status |
|---|---|---|
| v1.0 | Prize Vault & Escrow Engine | In Development |
| v1.0 | Developer Proof of Work Profiles | In Development |
| v1.0 | Paystack Split Disbursement | In Development |
| v1.1 | 48-Hour Media Vault + Trust Penalty | Planned |
| v1.1 | One-Click Internship Matching | Planned |
| v1.2 | Legacy Tracker (3-month check-ins) | Planned |
| v1.0 | Prize Vault & Escrow Engine | Done — live in production |
| v1.0 | Developer Proof of Work Profiles | Done |
| v1.0 | Paystack Split Disbursement (50/50 tranches) | Done |
| v1.1 | 48-Hour Media Vault + Trust Penalty | Done |
| v1.1 | One-Click Internship Matching | Done |
| v1.1 | Newsletters (subscribe + organizer blasts) | Done |
| v1.2 | Legacy Tracker (3-month check-ins + dispute window) | Done |
| v2.0 | **HackVillage DAO** — Community-driven Elite Organizer rankings | Moonshot |
| v2.0 | **Global Node** — Nigeria, Rwanda, cross-border prize handling | Moonshot |
| v2.0 | **AI Judging Co-pilot** — Pre-screen repos for quality & plagiarism | Moonshot |
Expand All @@ -260,14 +264,25 @@ pnpm install

### Environment Variables

Create a `.env.local` file at the project root:
Copy the template and fill in your values — `.env.example` documents every
variable the code reads (validated by `lib/env.ts`):

```bash
cp .env.example .env
```

The minimum set for a local boot:

```env
DATABASE_URL=postgresql://user:password@localhost:5432/hackvillage
DATABASE_URL=postgresql://user:***@localhost:5432/hackvillage
NEXTAUTH_SECRET= # generate: openssl rand -base64 32
NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Optional: without these, payments/chain run in documented simulation modes
PAYSTACK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY=pk_test_...
SMART_CONTRACT_ADDRESS=0x...
RPC_URL=https://...
RPC_URL=https://rpc.amoy.polygonscan.com
ATTESTER_PRIVATE_KEY=0x...
```

### Development
Expand Down
3 changes: 2 additions & 1 deletion app/(app)/dashboard/hackathons/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import Link from "next/link";
import { CalendarDays } from "lucide-react";

import { PrizeVerifiedBadge } from "@/components/patterns/prize-verified-badge";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
Expand Down Expand Up @@ -65,7 +66,7 @@ export default async function DashboardEventsPage() {
<div>
<div className="flex flex-wrap items-center gap-2">
{verified ? (
<Badge variant="brand">Prize Verified</Badge>
<PrizeVerifiedBadge />
) : (
<Badge variant="warning">{STATUS_LABELS[event.status]}</Badge>
)}
Expand Down
21 changes: 21 additions & 0 deletions app/(app)/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Skeleton, SkeletonCard } from "@/components/ui/skeleton";

export default function Loading() {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<Skeleton className="h-8 w-64" />
<Skeleton className="mt-2 h-4 w-48" />
</div>
<Skeleton className="h-10 w-36 rounded-full" />
</div>
<div className="grid gap-4 sm:grid-cols-3">
{[0, 1, 2].map((i) => (
<SkeletonCard key={i} className="h-28" />
))}
</div>
<SkeletonCard className="h-64" />
</div>
);
}
36 changes: 9 additions & 27 deletions app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { Metadata } from "next";
import Link from "next/link";
import { CalendarX2, Rocket } from "lucide-react";
import { CalendarX2 } from "lucide-react";

import { LegacyCheckinCard } from "@/components/legacy/legacy-cards";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardTitle } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { requireOnboardedUser } from "@/lib/auth/guards";
import { prisma } from "@/lib/db";
Expand Down Expand Up @@ -71,34 +70,17 @@ export default async function DashboardPage() {
</section>
) : null}

<EmptyState
icon={CalendarX2}
title={dueCheckins.length > 0 ? "" : "No active hackathons right now"}
description={
dueCheckins.length > 0
? ""
: "Prize Verified hackathons appear here the moment you register: active hackathons, teams, and in-flight payouts at a glance."
}
action={
dueCheckins.length > 0 ? undefined : (
{dueCheckins.length === 0 ? (
<EmptyState
icon={CalendarX2}
title="No active hackathons right now"
description="Prize Verified hackathons appear here the moment you register: active hackathons, teams, and in-flight payouts at a glance."
action={
<Link href="/hackathons">
<Button arrow>Browse Hackathons</Button>
</Link>
)
}
/>

{dueCheckins.length === 0 ? (
<Card>
<CardTitle className="flex items-center gap-2">
<Rocket aria-hidden className="size-5" /> The Full Loop Is Live
</CardTitle>
<CardDescription>
Escrowed prize pools, 50/50 payouts, structured judge feedback, verified Proof-of-Work
profiles, hiring intros, the 48-hour media standard, and 3-month legacy tracking. The
platform runs end to end. Next: production hardening (runbooks, load tests, audit).
</CardDescription>
</Card>
}
/>
) : null}
</div>
);
Expand Down
7 changes: 5 additions & 2 deletions app/(app)/dashboard/winnings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Link from "next/link";
import { HandCoins } from "lucide-react";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardTitle } from "@/components/ui/card";
import { DisputeForm } from "@/components/legacy/legacy-cards";
import { canOpenDispute } from "@/services/legacy/dispute-window";
Expand Down Expand Up @@ -96,7 +97,9 @@ export default async function WinningsPage() {
It takes a minute.
</CardDescription>
<Link href="/settings" className="mt-3 inline-block">
<Badge variant="brand">Set up payouts in Settings →</Badge>
<Button size="sm" arrow>
Set up payouts in Settings
</Button>
</Link>
</Card>
) : null}
Expand All @@ -108,7 +111,7 @@ export default async function WinningsPage() {
description="When you win a Prize Verified hackathon, the 50% instant tranche lands here the same day, tracked to the shilling."
action={
<Link href="/hackathons">
<Badge variant="brand">Find a hackathon</Badge>
<Button arrow>Find a hackathon</Button>
</Link>
}
/>
Expand Down
Loading
Loading