tpt-poa-registry-nz
GoDigital Power of Attorney registry for New Zealand, compliant with the Protection of Personal and Property Rights Act 1988. RealMe Verified identity, attorney acceptance workflow, institution verification API, HMAC-signed webhooks, and public QR-code verification. Built with Go + Next.js.
Languages
TPT POA Registry NZ
A digital registry for managing Power of Attorney (POA) instruments under the New Zealand Protection of Personal and Property Rights Act 1988. Both the granter and attorney must hold a RealMe Verified identity.
Features
| Feature | Description |
|---------|-------------|
| Digital POA Granting | Create enduring, general, property, personal/welfare, or specific powers of attorney |
| RealMe Verified Identity | Granter and attorney identities verified via RealMe SAML 2.0 |
| Attorney Acceptance Workflow | POAs start as pending_acceptance; attorneys receive a one-time link to accept or decline |
| QR Code Verification | Active POAs generate a scannable QR code linking to the public verification page |
| Public Verification Page | Anyone can verify a POA at /verify/:registryId — no login required |
| Full Audit Trail | Every lifecycle event (grant, accept, decline, amend, revoke, view) is logged with timestamp and actor |
| Institution Verification API | Banks and institutions verify POA validity via REST API with API-key auth and rate limiting |
| Bulk Verification | Verify up to 100 registry IDs in a single API call |
| Webhook Notifications | HMAC-SHA256-signed webhook delivery to subscribed institutions |
| Automatic Expiry | Background job marks POAs expired hourly when their expiry date passes |
| Human-Readable IDs | Sequential registry identifiers — e.g. POA-2026-00042 |
Architecture
┌─────────────────────────┐ ┌──────────────────────┐
│ Next.js Web Frontend │ │ Institution Portal │
│ (RealMe via backend) │ │ (API Key auth) │
└───────────┬─────────────┘ └──────────┬───────────┘
│ HTTP (proxy /api/*) │ HTTP
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Go API Server (:8082) │
│ ┌──────────┐ ┌────────────┐ ┌────────────────────────┐ │
│ │ Auth │ │ POA CRUD + │ │ Institution Registry │ │
│ │ Handlers │ │ Acceptance │ │ Bulk Verify + Webhook │ │
│ └────┬─────┘ └─────┬──────┘ └───────────┬────────────┘ │
│ │ │ │ │
│ ┌────▼─────────────▼─────────────────────▼──────────┐ │
│ │ Services Layer │ │
│ │ POAService · WebhookService · ExpiryJob │ │
│ └────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼───────────────────────────┐ │
│ │ Repository Layer (pgx/pgxpool) │ │
│ └────────────────────────┬───────────────────────────┘ │
└───────────────────────────┼──────────────────────────────┘
│
┌──────▼──────┐
│ PostgreSQL │
└─────────────┘
API Endpoints
RealMe Authentication
| Method | Path | Description |
|--------|------|-------------|
| GET | /auth/login | Initiate RealMe login |
| GET | /auth/callback | RealMe ACS callback |
| GET | /auth/logout | Clear session |
| GET | /auth/metadata | SP metadata XML |
| GET | /auth/status | Auth status (requires login) |
POA Management (RealMe Verified required)
| Method | Path | Description |
|--------|------|-------------|
| POST | /poa/grant | Create new POA (returns acceptance token) |
| GET | /poa/granted | List POAs where user is granter |
| GET | /poa/held | List POAs where user is attorney |
| GET | /poa/search?q=... | Search POA records |
| GET | /poa/registry/{registryId} | Get POA by registry ID (e.g. POA-2026-00042) |
| GET | /poa/{id} | Get POA by UUID |
| PUT | /poa/{id}/amend | Amend POA scope/details |
| POST | /poa/{id}/revoke | Revoke POA |
| GET | /poa/{id}/audit | Audit log for POA |
| GET | /poa/{id}/acceptance-link | Get active acceptance link (granter only) |
Attorney Acceptance (public — no auth)
| Method | Path | Description |
|--------|------|-------------|
| GET | /poa/accept/{token} | View POA invitation details |
| POST | /poa/accept/{token} | Accept POA — transitions status to active |
| POST | /poa/decline/{token} | Decline POA (with reason) |
Institution Verification (API key required — X-API-Key header)
Rate limited to 100 requests / minute per API key.
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/v1/registry/lookup/{registryId} | Look up POA details |
| GET | /api/v1/registry/verify?registryId=...&granterName=... | Verify POA validity |
| POST | /api/v1/registry/verify/batch | Bulk verify up to 100 registry IDs |
Webhook Management (API key required)
| Method | Path | Description |
|--------|------|-------------|
| POST | /webhook/subscribe | Create webhook subscription |
| GET | /webhook/subscriptions?institutionId=... | List subscriptions |
POA Lifecycle
[Grant]
│
▼
pending_acceptance ──accept──▶ active ──amend──▶ amended
│ │ │
└──decline──▶ declined └──revoke──▶ revoked
│
(expiry date passes)
│
▼
expired
| Status | Description |
|--------|-------------|
| pending_acceptance | POA granted; awaiting attorney acceptance |
| active | Attorney accepted; POA is valid |
| amended | Granter modified scope/details |
| declined | Attorney declined the appointment |
| revoked | Granter revoked a revocable POA |
| expired | Past the specified expiry date (set automatically) |
| suspended | Temporarily suspended by admin |
Webhook Events
Webhooks deliver HMAC-SHA256-signed payloads to subscribed institution URLs
via the X-TPT-Signature header.
| Event | Trigger |
|-------|---------|
| poa.granted | New POA created (status: pending_acceptance) |
| poa.accepted | Attorney accepted — POA now active |
| poa.declined | Attorney declined |
| poa.amended | Granter amended scope or details |
| poa.revoked | Granter revoked the POA |
| * | Subscribe to all events |
Getting Started
Prerequisites
- Go 1.23+
- PostgreSQL 15+
- Node.js 20+ and pnpm 9+
- RealMe SP certificates (or use the included mock IdP for local dev)
1 — Clone and configure
git clone https://github.com/tpt-nz/tpt-poa-registry-nz.git
cd tpt-poa-registry-nz
cp .env.example .env
# Edit .env with your values
2 — Database
# Start PostgreSQL (or use Docker: make dev)
createdb tptnz
# Run migrations
psql "$DATABASE_URL" -f migrations/001_init.sql
psql "$DATABASE_URL" -f migrations/002_attorney_acceptance.sql
3 — Start the Go API server
go mod tidy
go run ./cmd/server
# Listening on :8082
4 — Start the mock IdP (for local RealMe)
cd packages/realme-go
go run ./testenv/ -addr :8081
5 — Start the Next.js frontend
pnpm install
pnpm --filter web dev
# Listening on :3000
Docker (all-in-one)
make dev # docker compose up -d (PostgreSQL + app)
make stop # docker compose down
make migrate # run Atlas migrations
Environment Variables
Go API Server
| Variable | Default | Description |
|----------|---------|-------------|
| LISTEN_ADDR | :8082 | HTTP listen address |
| DATABASE_URL | postgres://tptnz:tptnz_dev@localhost:5432/tptnz?sslmode=disable | PostgreSQL DSN |
| REALME_ENVIRONMENT | mts | RealMe environment (mts | ite | production) |
| REALME_CERT_FILE | certs/sp.crt | SP certificate path |
| REALME_KEY_FILE | certs/sp.key | SP private key path |
| REALME_ENTITY_ID | http://localhost:8082/auth/metadata | SP entity ID |
| REALME_ACS_URL | http://localhost:8082/auth/callback | ACS callback URL |
| REALME_IDP_METADATA_URL | http://localhost:8081/metadata | IdP metadata URL |
| POA_API_KEY | dev-api-key | API key for institution endpoints |
Next.js Frontend
| Variable | Description |
|----------|-------------|
| BACKEND_URL | Base URL of the Go API — server-side only, never sent to the browser |
| BACKEND_API_KEY | API key for the server-side public verification page (matches POA_API_KEY) |
| NEXT_PUBLIC_SITE_URL | Public URL of the frontend — used for generating absolute QR code URLs |
RealMe Registration
MTS (Development / Mock IdP)
-
Generate a self-signed SP certificate:
mkdir -p certs openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout certs/sp.key -out certs/sp.crt \ -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost" -
Start the mock IdP bundled in this repo:
cd packages/realme-go go run ./testenv/ -addr :8081 -
Set
REALME_IDP_METADATA_URL=http://localhost:8081/metadatain your.env.
ITE (Pre-Production)
- Register at the RealMe Developer Portal.
- Submit your SP metadata (available at
GET /auth/metadata) to DIA. - DIA will provide the ITE IdP metadata URL.
- Use a properly named certificate:
ite.{service-name}.{org-domain}.nz - Set
REALME_ENVIRONMENT=iteand updateREALME_IDP_METADATA_URL.
Production
Same as ITE with REALME_ENVIRONMENT=production and the production IdP metadata URL from DIA.
Development
make test # go test ./...
make test-race # go test -race ./...
make lint # golangci-lint run ./...
make vet # go vet ./...
make build # build binary to bin/
make tools # install air, atlas, golangci-lint
Contributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.