Myinfo (Singpass) v5 integration in Next.js 16
A production-ready Next.js application implementing MyInfo (Singpass) v5 integration with full OAuth2.1/OIDC FAPI 2.0 compliance.
Repository: https://github.com/sebastiansieber/myinfo-integration
Table of Contents
- Overview
- Features
- Prerequisites
- Installation
- Configuration
- Deployment
- Testing
- Architecture
- API Routes
- Security Features
- Troubleshooting
- Production Checklist
Overview
This application demonstrates a complete MyInfo v5 integration following the OAuth2.1 Authorization Code Flow with PKCE, DPoP (Demonstration of Proof-of-Possession), and JWT-based client authentication. It retrieves user data from Singapore's MyInfo service after Singpass authentication.
Key Standards Implemented:
- OAuth 2.1 (RFC 6749 + enhancements)
- OpenID Connect (OIDC)
- FAPI 2.0 (Financial-grade API)
- PKCE (RFC 7636)
- DPoP (RFC 9449)
- JWT Client Authentication (RFC 7523)
Features
✅ Complete OAuth2.1/OIDC Flow
- Authorization with PKCE (Proof Key for Code Exchange)
- JWT-based client assertion (no static secrets)
- DPoP token binding for enhanced security
- Adaptive Bearer/DPoP authorization (staging compatibility)
✅ Secure Data Handling
- JWE (JSON Web Encryption) for data encryption
- JWS (JSON Web Signature) for data integrity
- Ephemeral key generation per session
- Secure session management
✅ Production Ready
- TypeScript for type safety
- Docker support with Docker Compose
- Environment-based configuration
- Comprehensive error handling
- JWKS endpoint for public key distribution
Prerequisites
Before you begin, ensure you have:
- Node.js 18.x or higher
- npm or yarn package manager
- Docker and Docker Compose (for Docker deployment)
- Singpass Developer Portal Account (Register here)
- OpenSSL (for key generation)
- A publicly accessible domain with HTTPS (required for Singpass)
Installation
1. Clone the Repository
git clone <repository-url>
cd myinfo-test
2. Install Dependencies
npm install
This installs:
next- Next.js frameworkreact&react-dom- React libraryaxios- HTTP clientjose- JOSE (JWT/JWE/JWS) library- TypeScript and related types
Configuration
Singpass Developer Portal Setup
Step 1: Create a New App
- Log in to the Singpass Developer Portal
- Navigate to My Apps → Create New App
- Fill in the app details:
- App Name:
test-app(or your preferred name) - Industry: Select your industry
- App Category: General
- Description: Brief description of your app
- App Name:
Step 2: Configure App Settings
Navigate to your app's configuration page and set:
General Settings:
- Profile Type: UUID only (or UUID + NRIC based on your needs)
- Authentication Type: 1FA (or 2FA for production)
- Site URL(s):
https://yourdomain.com - Redirect URL(s):
https://yourdomain.com/redirect- ⚠️ Must be HTTPS (except localhost for development)
- Must match exactly what you use in the authorization request
- JWKS Endpoint:
https://yourdomain.com/.well-known/jwks.json- This is where you'll serve your public keys
- Must be publicly accessible (no authentication required)
- Must be on port 443 (HTTPS)
Scopes Selection:
- Select the data attributes you need (e.g.,
name,email,mobileno) - For this test app:
openid,name
App Purpose:
- Purpose Description: "Test MyInfo integration" (or your actual purpose)
- This is shown to users during consent
Security:
- Assurance Level: 2 (standard for most apps)
Step 3: Note Your Credentials
After creating the app, you'll receive:
- App ID (Client ID): e.g.,
KD5ZM*************************heqQV - This is your
MYINFO_CLIENT_ID
Key Generation
MyInfo v5 requires two ECDSA P-256 key pairs:
- Signing Key: For client assertion JWTs
- Encryption Key: For decrypting MyInfo data
Generate Keys with OpenSSL
# Generate signing key pair (PKCS8 format for private key)
openssl ecparam -name prime256v1 -genkey -noout -out signing-private.pem
openssl pkcs8 -topk8 -nocrypt -in signing-private.pem -out signing-private-pkcs8.pem
openssl ec -in signing-private-pkcs8.pem -pubout -out signing-public.pem
# Generate encryption key pair (PKCS8 format)
openssl ecparam -name prime256v1 -genkey -noout -out encryption-private.pem
openssl pkcs8 -topk8 -nocrypt -in encryption-private.pem -out encryption-private-pkcs8.pem
openssl ec -in encryption-private-pkcs8.pem -pubout -out encryption-public.pem
Alternative: Use SEC1 Format (also supported)
# If you already have SEC1 format keys, they work too
openssl ecparam -name prime256v1 -genkey -noout -out signing-private-sec1.pem
openssl ec -in signing-private-sec1.pem -pubout -out signing-public.pem
Important Notes:
- ⚠️ Keep private keys secure: Never commit them to version control
- 🔄 Use different keys for staging and production
- 📝 PKCS8 format is recommended but SEC1 format also works
- 🔑 The public keys will be served via your JWKS endpoint
Environment Variables
Create .env file
Create a .env file in the project root (for Docker) or .env.local (for local development):
# MyInfo Configuration
MYINFO_ENV=staging
MYINFO_CLIENT_ID=your_client_id_from_portal
MYINFO_REDIRECT_URI=https://yourdomain.com/redirect
MYINFO_JWKS_URI=https://yourdomain.com/.well-known/jwks.json
MYINFO_SCOPES=openid name
# Private Keys (PKCS8 or SEC1 format)
MYINFO_SIGNING_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\n******\n-----END EC PRIVATE KEY-----"
MYINFO_ENCRYPTION_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\n******\n-----END EC PRIVATE KEY-----"
# Next.js Configuration
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
For Production Deployment
If using PKCS8 format (recommended):
MYINFO_SIGNING_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n******\n-----END PRIVATE KEY-----"
Important Environment Variable Notes:
-
Multi-line Keys:
- Wrap the entire key (including headers) in quotes
- Use actual newlines or
\ncharacters - Docker/Next.js will handle both formats
-
MYINFO_REDIRECT_URI:
- Must match exactly what's in the Singpass portal
- Must be HTTPS (except localhost)
- No trailing slash
-
MYINFO_JWKS_URI:
- Must be publicly accessible
- Must return your public keys in JWKS format
- Will be accessed by Singpass servers
-
NEXT_PUBLIC_SITE_URL:
- Used for OAuth redirects
- Must be your public domain
- Required for Docker deployments behind reverse proxy
Deployment
Docker Deployment
This is the recommended deployment method for production.
1. Build the Docker Image
docker-compose build
2. Start the Application
docker-compose up -d
The app will be available on port 3000 (configure your reverse proxy to forward to it).
3. Configure Reverse Proxy (e.g., Traefik, Nginx)
Example Traefik labels in docker-compose.yml:
labels:
- "traefik.enable=true"
- "traefik.http.routers.myinfo.rule=Host(`your-domain.com`)"
- "traefik.http.routers.myinfo.entrypoints=websecure"
- "traefik.http.routers.myinfo.tls.certresolver=letsencrypt"
- "traefik.http.services.myinfo.loadbalancer.server.port=3000"
Example Nginx configuration:
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
4. Verify JWKS Endpoint
After deployment, verify your JWKS endpoint is accessible:
curl https://yourdomain.com/.well-known/jwks.json
Expected response:
{
"keys": [
{
"kty": "EC",
"x": "...",
"y": "...",
"crv": "P-256",
"kid": "sig-key-1",
"use": "sig",
"alg": "ES256"
},
{
"kty": "EC",
"x": "...",
"y": "...",
"crv": "P-256",
"kid": "enc-key-1",
"use": "enc",
"alg": "ECDH-ES+A256KW"
}
]
}
⚠️ Verify that the d parameter (private key) is NOT present in the JWKS!
Vercel/Railway Deployment
1. Set Environment Variables
In your deployment platform's dashboard, add all the environment variables from your .env file.
Important for multi-line keys:
# Use literal \n for newlines in the platform UI
MYINFO_SIGNING_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\nMHcCAQEE...\n-----END EC PRIVATE KEY-----"
2. Deploy
# Vercel
vercel --prod
# Railway
railway up
3. Configure Custom Domain
Set up your custom domain in the platform's DNS settings and ensure HTTPS is enabled.
Testing
Testing in Staging Environment
1. Navigate to Your App
https://yourdomain.com
2. Click "Login with Singpass"
This initiates the OAuth flow and redirects you to Singpass staging.
3. Use Test Credentials
In the Singpass Developer Portal, you'll find test personas with demo credentials:
- Test NRIC numbers
- Demo Singpass passwords
- Various data scenarios
Example Test Users:
- NRIC:
S9812381D(use the test credentials provided in the portal)
4. Grant Consent
After login, you'll see a consent screen showing:
- The data being requested (e.g., name)
- Your app's purpose
- Option to approve or deny
5. View Retrieved Data
After granting consent, you'll be redirected back to your app with the name displayed:
Success!
Name retrieved from MyInfo: John Doe
Troubleshooting Test Issues
If you get "Request parameters invalid" (PX-E1000):
- Verify your redirect URI matches exactly
- Check that JWKS endpoint is accessible
- Ensure environment variables are loaded
If you get "Invalid JWKS object" (PX-E0101):
- Verify JWKS endpoint returns valid JSON
- Check that private keys (
dparameter) are NOT in the JWKS - Ensure both signing and encryption keys are present
If you get 401 on userinfo:
- This is expected in staging if token_type is "Bearer"
- The app automatically adapts to Bearer tokens in staging
If name shows "Unknown":
- Check server logs for decryption errors
- Verify encryption private key is correct
- Ensure MyInfo JWKS is accessible for signature verification
Architecture
OAuth 2.1 Flow Overview
┌─────────┐ ┌──────────────┐
│ │ 1. Initiate Login │ │
│ User │───────────────────────────────────────>│ Your App │
│ Browser │ │ │
└─────────┘ └──────────────┘
│ │
│ 2. Redirect to Singpass │
│ (with PKCE code_challenge) │
│<───────────────────────────────────────────────────┘
│
│ ┌──────────────┐
│ 3. Login & Consent │ │
│─────────────────────────────────────────────>│ Singpass │
│ │ (MyInfo) │
│ 4. Redirect back with auth code │ │
│<─────────────────────────────────────────────│ │
│ └──────────────┘
│
│ ┌──────────────┐
│ 5. Exchange code for token │ │
│ (with code_verifier + client_assertion │ Your App │
│ + DPoP proof) │ Backend │
│─────────────────────────────────────────────>│ │
│ └──────┬───────┘
│ │
│ │ 6. Request person data
│ │ (with access_token
│ ┌──────▼───────┐ + DPoP proof)
│ │ │
│ │ Singpass │◄────┐
│ │ (MyInfo) │ │
│ │ │─────┘
│ └──────┬───────┘
│ │
│ 7. Display data ┌──────▼───────┐
│<─────────────────────────────────────────────│ Your App │
│ │ │
│ └──────────────┘
Detailed Flow Steps
Step 1: Authorization Request
Route: /api/auth/myinfo
- Generate ephemeral ECDSA P-256 key pair (for DPoP)
- Generate PKCE code_verifier and code_challenge
- Store session data (code_verifier, state, ephemeral keys)
- Redirect to Singpass
/authendpoint with:client_id: Your App IDscope:openid name(or other requested scopes)redirect_uri: Your callback URLresponse_type:codecode_challenge: SHA-256 hash of code_verifiercode_challenge_method:S256state: Random string for CSRF protectionnonce: Random string for ID token validation
Step 2: User Authentication & Consent
User authenticates with Singpass and grants consent to share data.
Step 3: Authorization Callback
Route: /redirect
Singpass redirects back with:
code: Authorization codestate: Your state value (verify it matches)
Step 4: Token Exchange
Internal: exchangeToken() function
-
Generate Client Assertion JWT:
- Signed with your signing private key
- Contains
cnf.jktclaim with ephemeral key thumbprint - Claims:
iss,sub,aud,iat,exp,jti,cnf
-
Generate DPoP Proof JWT (for token request):
- Signed with ephemeral private key
- Contains ephemeral public key in header
- Claims:
jti,htm,htu,iat,exp
-
POST to
/tokenendpoint with:- Form data:
grant_type,code,redirect_uri,client_id,code_verifier,client_assertion_type,client_assertion - Header:
DPoP: <dpop_proof_jwt>
- Form data:
-
Receive:
access_token: Bearer token (in staging) or DPoP-bound token (in production)id_token: Encrypted JWT with user infotoken_type: "Bearer" or "DPoP"
Step 5: Person Data Retrieval
Internal: getPersonData() function
-
Check
token_type:- If "DPoP": Generate new DPoP proof with
athclaim (access token hash) - If "Bearer": Use standard Bearer authorization
- If "DPoP": Generate new DPoP proof with
-
GET
/userinfoendpoint with:- Header:
Authorization: Bearer <token>orAuthorization: DPoP <token> - Header (if DPoP):
DPoP: <dpop_proof_jwt>
- Header:
-
Receive encrypted response (JWE format)
Step 6: Data Decryption & Verification
Internal: decryptAndVerifyPersonData() function
-
Decrypt JWE:
- Use your encryption private key
- Extracts the JWS (signed payload)
-
Verify JWS signature:
- Fetch MyInfo's public keys from their JWKS endpoint
- Verify signature with matching key
- Extract person data payload
-
Parse Person Data:
- Extract user attributes (name, etc.)
- Structure:
{ person_info: { name: { value: "..." } } }
Step 7: Display Data
Redirect user to homepage with name in query params:
https://yourdomain.com/?name=John+Doe
API Routes
/api/auth/myinfo (GET)
Initiates the MyInfo login flow.
Response: Redirects to Singpass authorization endpoint
Query Parameters Generated:
client_id: Your App IDscope: Requested scopesredirect_uri: Callback URLresponse_type:codecode_challenge: PKCE challengecode_challenge_method:S256state: CSRF tokennonce: ID token validation token
/redirect (GET)
OAuth callback handler. Processes the authorization code and retrieves user data.
Query Parameters Expected:
code: Authorization code from Singpassstate: Must match the state sent in authorization request
Success Response: Redirects to /?name=<user_name>
Error Responses:
/?error=invalid_state- State mismatch (CSRF attack)/?error=invalid_session- Session not found or expired/?error=token_exchange_failed- Token exchange failed/?error=callback_processing_failed- General error
/.well-known/jwks.json (GET)
Serves public keys in JWKS format for Singpass to encrypt data.
Response Example:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "base64url-encoded-x-coordinate",
"y": "base64url-encoded-y-coordinate",
"kid": "sig-key-1",
"use": "sig",
"alg": "ES256"
},
{
"kty": "EC",
"crv": "P-256",
"x": "base64url-encoded-x-coordinate",
"y": "base64url-encoded-y-coordinate",
"kid": "enc-key-1",
"use": "enc",
"alg": "ECDH-ES+A256KW"
}
]
}
Important:
- Must be publicly accessible (no authentication)
- Must NOT contain private key components (
dparameter) - Cached by Singpass for up to 1 hour
- Must be HTTPS with valid certificate
Security Features
PKCE (Proof Key for Code Exchange)
Purpose: Prevents authorization code interception attacks
Implementation:
- Generate random
code_verifier(43-128 characters) - Compute
code_challenge= BASE64URL(SHA256(code_verifier)) - Send
code_challengein authorization request - Send
code_verifierin token request - Singpass verifies: SHA256(code_verifier) === code_challenge
Code Location: lib/myinfo/crypto.ts - generatePKCE()
DPoP (Demonstration of Proof-of-Possession)
Purpose: Prevents token theft by binding tokens to a specific client-held key
Implementation:
- Generate ephemeral ECDSA P-256 key pair per session
- Include ephemeral public key thumbprint in client assertion (
cnf.jkt) - Sign each request (token, userinfo) with ephemeral private key
- Include ephemeral public key in DPoP JWT header
- Singpass validates that the same key is used throughout the flow
Code Location:
lib/myinfo/crypto.ts-generateEphemeralKeyPair(),generateDpopProof()lib/myinfo/service.ts-exchangeToken(),getPersonData()
JWT Client Authentication
Purpose: Eliminates static client secrets, uses asymmetric cryptography
Implementation:
- Sign client assertion JWT with your signing private key
- Singpass fetches your public key from JWKS endpoint
- Singpass verifies JWT signature
- Ensures request comes from legitimate client
Claims in Client Assertion:
iss: Your client ID (issuer)sub: Your client ID (subject)aud: Token endpoint URLiat: Issued at timestampexp: Expiry timestamp (5 minutes)jti: Unique JWT ID (prevents replay)cnf.jkt: Ephemeral DPoP key thumbprint
Code Location: lib/myinfo/crypto.ts - generateClientAssertion()
JWE/JWS Encryption
Purpose: End-to-end encryption and data integrity verification
Flow:
- MyInfo signs person data with their private key (JWS)
- MyInfo encrypts signed data with your public key (JWE)
- You receive encrypted data
- You decrypt with your private key (JWE → JWS)
- You verify signature with MyInfo's public key (JWS → data)
Algorithms:
- JWE: ECDH-ES+A256KW (Elliptic Curve Diffie-Hellman Ephemeral Static + AES256 Key Wrap)
- JWS: ES256 (ECDSA with P-256 curve and SHA-256)
Code Location: lib/myinfo/service.ts - decryptAndVerifyPersonData()
Session Management
Current Implementation: In-memory store (suitable for testing)
Structure:
{
state: {
codeVerifier: string,
state: string,
ephemeralKeyPair: {
privateKey: CryptoKey,
publicKey: CryptoKey,
publicJwk: JsonWebKey,
thumbprint: string
}
}
}
For Production: Replace with Redis, database, or encrypted cookies
Code Location: lib/myinfo/session.ts
Troubleshooting
Common Issues
1. "Request parameters invalid" (PX-E1000)
Symptoms: Error page from Singpass after clicking login
Causes:
- Redirect URI mismatch (must match exactly in portal)
- Client ID incorrect or not configured
- PKCE parameters malformed
Solutions:
- Verify
MYINFO_REDIRECT_URImatches portal configuration - Check
MYINFO_CLIENT_IDis correct - Ensure no trailing slashes in URIs
2. "Invalid JWKS object" (PX-E0101)
Symptoms: Error page from Singpass after clicking login
Causes:
- JWKS endpoint not accessible
- JWKS contains private key components
- Invalid JSON format
- Wrong key types or algorithms
Solutions:
# Test JWKS endpoint
curl https://yourdomain.com/.well-known/jwks.json
# Verify response structure
# Should have two keys: sig and enc
# Should NOT have "d" parameter (private key)
Verify JWKS format:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...", // Public key X coordinate
"y": "...", // Public key Y coordinate
"kid": "sig-key-1",
"use": "sig",
"alg": "ES256"
// NO "d" parameter!
},
// ... encryption key
]
}
3. Token Exchange 401 Error
Symptoms: token_exchange_failed error after Singpass redirect
Causes:
- Client assertion signature invalid
- Private key format incorrect
- DPoP proof malformed
- Client assertion thumbprint mismatch
Solutions:
- Verify private keys are in PKCS8 or SEC1 format
- Check key file has proper header/footer
- Ensure ephemeral key thumbprint is correct in client assertion
- Review server logs for detailed error messages
4. Userinfo 401 Error
Symptoms: token_exchange_failed error after token is obtained
Causes:
- Using DPoP authorization with Bearer token (staging issue)
- Access token expired
- DPoP proof signature invalid
Solutions:
- Check that the app uses Bearer scheme when
token_typeis "Bearer" - Verify adaptive authorization logic is working
- In staging, expect Bearer tokens (not DPoP-bound)
5. Name Shows "Unknown"
Symptoms: Success page shows "Unknown" instead of actual name
Causes:
- Data decryption failed
- JWS signature verification failed
- Person data structure different than expected
- Encryption private key incorrect
Solutions:
- Check server logs for decryption errors
- Verify
MYINFO_ENCRYPTION_PRIVATE_KEYis correct - Ensure MyInfo JWKS endpoint is accessible
- Check that person data structure matches expected format
Add temporary logging:
// In app/redirect/route.ts
console.log('Person data:', JSON.stringify(personData, null, 2));
6. Environment Variables Not Loading
Symptoms: undefined values in configuration
For Docker:
- Verify
.envfile exists in project root - Check
docker-compose.ymlhasenv_file: - .env - Rebuild container:
docker-compose up --build
For Local Development:
- Use
.env.localfile (not.env) - Restart dev server:
npm run dev
For Vercel/Railway:
- Set environment variables in platform dashboard
- Use
\nfor newlines in multi-line keys - Redeploy after updating variables
Debug Mode
To enable detailed logging, uncomment console.log statements in:
lib/myinfo/service.ts- Token exchange and userinfo requestslib/myinfo/crypto.ts- Key generation and JWT creationapp/redirect/route.ts- Callback processing
Example:
// Decode access token payload
const accessTokenParts = response.data.access_token.split('.');
const accessTokenPayload = JSON.parse(
Buffer.from(accessTokenParts[1], 'base64url').toString()
);
console.log('Access Token Payload:', accessTokenPayload);
Production Checklist
Before Going Live
1. Security Audit
- [ ] Private keys secured (not in version control)
- [ ] Different keys for staging and production
- [ ] Environment variables properly configured
- [ ] HTTPS enabled on all endpoints
- [ ] JWKS endpoint publicly accessible
- [ ] No private keys in JWKS response
- [ ] Session storage is secure (not in-memory)
2. Singpass Portal Configuration
- [ ] Production app created and configured
- [ ] Correct production App ID in environment
- [ ] Production redirect URI matches exactly
- [ ] Production JWKS URL configured
- [ ] All required scopes approved
- [ ] App purpose clearly stated
- [ ] Contact emails updated
3. Infrastructure
- [ ] HTTPS certificate valid and not expired
- [ ] Domain DNS configured correctly
- [ ] Reverse proxy configured (if applicable)
- [ ] Load balancer configured (if applicable)
- [ ] Firewall rules allow HTTPS traffic
- [ ] Health check endpoint configured
4. Testing
- [ ] Full OAuth flow tested in production
- [ ] Test with multiple user accounts
- [ ] Error scenarios tested (deny consent, network errors)
- [ ] JWKS endpoint tested from external network
- [ ] Token exchange tested
- [ ] Data decryption tested
- [ ] Performance tested under load
5. Monitoring
- [ ] Error logging configured
- [ ] Application monitoring set up
- [ ] Alerts configured for failures
- [ ] Audit logging for data access
- [ ] Rate limiting configured
6. Compliance
- [ ] Privacy policy updated
- [ ] Terms of service updated
- [ ] Data retention policy defined
- [ ] User consent recorded
- [ ] PDPA compliance verified
7. Production Endpoints
Update environment variables to use production endpoints:
MYINFO_ENV=production
MYINFO_CLIENT_ID=<production_client_id>
MYINFO_REDIRECT_URI=https://yourdomain.com/redirect
MYINFO_JWKS_URI=https://yourdomain.com/.well-known/jwks.json
Production Singpass endpoints (automatically used when MYINFO_ENV=production):
- Authorization:
https://id.singpass.gov.sg/auth - Token:
https://id.singpass.gov.sg/token - Userinfo:
https://id.singpass.gov.sg/userinfo - JWKS:
https://id.singpass.gov.sg/.well-known/keys
Additional Resources
Official Documentation
- Singpass Developer Portal
- MyInfo Technical Documentation
- FAPI 2.0 Specification
- OAuth 2.1 Draft
- DPoP RFC 9449
Support
- Singpass Partner Support: partnersupport.singpass.gov.sg
- Developer Portal: Check announcements for maintenance windows
- Technical Issues: Contact Singpass support with error codes
Key Specifications
- PKCE: RFC 7636
- JWT: RFC 7519
- JWE: RFC 7516
- JWS: RFC 7515
- DPoP: RFC 9449
- Client Authentication: RFC 7523
License
This project is for demonstration purposes. Ensure compliance with Singpass terms of service and Singapore's PDPA when using MyInfo data.
Contributing
When contributing, ensure:
- No private keys in commits
- TypeScript types properly defined
- Error handling comprehensive
- Security best practices followed
- Documentation updated
Last Updated: October 2025
MyInfo Version: v5
Next.js Version: 16.0.1