Successfully implemented the backend notification subscription system for the Tikka raffle platform. Users can subscribe to receive notifications when a raffle ends or when they win.
002_notifications.sql: Creates the notifications table- Stores user subscriptions with raffle_id and user_address
- Supports multiple notification channels (email, push)
- Unique constraint prevents duplicate subscriptions
- Indexed for efficient queries
- RLS enabled for future security policies
notification.service.ts: Supabase integration servicesubscribe(): Create new subscriptionunsubscribe(): Remove subscriptiongetUserSubscriptions(): Get all user subscriptionsgetSubscription(): Get specific subscriptiongetRaffleSubscribers(): Get all subscribers for a raffleisSubscribed(): Check subscription status
subscribe.dto.ts: Request validationraffleId: Required integerchannel: Optional ('email' | 'push')
notifications.controller.ts: HTTP endpointsPOST /notifications/subscribe: Subscribe to raffleDELETE /notifications/subscribe/:raffleId: UnsubscribeGET /notifications/subscriptions: List user subscriptions
notifications.service.ts: Business logic layer- Delegates to core notification service
- Handles request/response transformation
notifications.module.ts: NestJS module configuration- Registers controller and services
- Exports services for use by other modules
- Updated
app.module.tsto include NotificationsModule - Integrated with existing JWT authentication
- Uses existing Supabase connection
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
raffle_id INTEGER NOT NULL,
user_address VARCHAR(56) NOT NULL,
channel VARCHAR(20) NOT NULL DEFAULT 'email',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT unique_raffle_user UNIQUE (raffle_id, user_address)
);idx_notifications_raffle_id: Fast lookup by raffleidx_notifications_user_address: Fast lookup by useridx_notifications_created_at: Ordered by creation time
Subscribe to raffle notifications
Authentication: Required (JWT Bearer token)
Request Body:
{
"raffleId": 123,
"channel": "email"
}Response (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"raffle_id": 123,
"user_address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"channel": "email",
"created_at": "2024-02-25T10:30:00.000Z"
}Error Responses:
401 Unauthorized: Missing or invalid JWT token409 Conflict: Already subscribed to this raffle400 Bad Request: Invalid request body
Unsubscribe from raffle notifications
Authentication: Required (JWT Bearer token)
Parameters:
raffleId: Raffle ID (integer)
Response (204 No Content): Empty body
Error Responses:
401 Unauthorized: Missing or invalid JWT token
Get all user subscriptions
Authentication: Required (JWT Bearer token)
Response (200 OK):
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"raffle_id": 123,
"user_address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"channel": "email",
"created_at": "2024-02-25T10:30:00.000Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"raffle_id": 456,
"user_address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"channel": "push",
"created_at": "2024-02-25T11:00:00.000Z"
}
]Error Responses:
401 Unauthorized: Missing or invalid JWT token
All endpoints require JWT authentication:
- Token must be provided in
Authorization: Bearer <token>header - Token is validated by
JwtAuthGuard(global guard) - User address is extracted from JWT payload via
@CurrentUser('address')decorator - Tokens are issued by the
/auth/verifyendpoint after SIWS verification
backend/
├── database/
│ └── migrations/
│ └── 002_notifications.sql # Database migration
├── src/
│ ├── api/
│ │ └── rest/
│ │ └── notifications/
│ │ ├── dto/
│ │ │ ├── subscribe.dto.ts # Request validation
│ │ │ └── index.ts
│ │ ├── notifications.controller.ts # HTTP endpoints
│ │ ├── notifications.service.ts # Business logic
│ │ └── notifications.module.ts # Module config
│ ├── services/
│ │ └── notification.service.ts # Supabase integration
│ └── app.module.ts # Updated with NotificationsModule
Execute the migration in Supabase SQL Editor:
# Copy the contents of backend/database/migrations/002_notifications.sql
# Paste into Supabase SQL Editor and runOr use the Supabase CLI:
supabase db pushEnsure these variables are set in .env:
# Supabase (already configured)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# JWT (already configured)
JWT_SECRET=your-jwt-secret
JWT_EXPIRES_IN=7dDependencies are already installed (NestJS, Supabase client):
cd backend
npm installnpm run devThe notification endpoints will be available at:
http://localhost:3001/notifications/subscribehttp://localhost:3001/notifications/subscribe/:raffleIdhttp://localhost:3001/notifications/subscriptions
- Get JWT Token (sign in first):
# Get nonce
curl http://localhost:3001/auth/nonce?address=YOUR_ADDRESS
# Sign message with wallet and verify
curl -X POST http://localhost:3001/auth/verify \
-H "Content-Type: application/json" \
-d '{
"address": "YOUR_ADDRESS",
"signature": "YOUR_SIGNATURE",
"nonce": "NONCE_FROM_STEP_1"
}'
# Save the accessToken from response- Subscribe to Raffle:
curl -X POST http://localhost:3001/notifications/subscribe \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"raffleId": 1,
"channel": "email"
}'- Get Subscriptions:
curl http://localhost:3001/notifications/subscriptions \
-H "Authorization: Bearer YOUR_JWT_TOKEN"- Unsubscribe:
curl -X DELETE http://localhost:3001/notifications/subscribe/1 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"- Run database migration
- Start backend server
- Authenticate and get JWT token
- Subscribe to a raffle
- Verify subscription in database
- Get user subscriptions
- Try subscribing again (should return existing)
- Unsubscribe from raffle
- Verify subscription removed
- Test without authentication (should fail)
- Test with invalid raffle ID
- Test with invalid channel
The backend endpoints match the frontend expectations:
| Frontend Call | Backend Endpoint |
|---|---|
subscribeToRaffle() |
POST /notifications/subscribe |
unsubscribeFromRaffle() |
DELETE /notifications/subscribe/:raffleId |
getUserSubscriptions() |
GET /notifications/subscriptions |
Response formats match the TypeScript interfaces defined in the frontend.
The notification system is ready for delivery implementation:
// Example: Send email when raffle ends
async function notifyRaffleEnd(raffleId: number) {
const subscribers = await notificationService.getRaffleSubscribers(raffleId);
for (const sub of subscribers) {
if (sub.channel === 'email') {
await emailService.send({
to: sub.user_address, // or lookup email from user profile
subject: `Raffle #${raffleId} has ended!`,
template: 'raffle-ended',
data: { raffleId }
});
}
}
}Implemented: PushNotificationService (FCM) and device token registration endpoints.
POST /notifications/device-token(auth required) - register user device tokenDELETE /notifications/device-token(auth required) - remove user device tokenPOST /notifications/subscribe/DELETE /notifications/subscribe/:raffleId/GET /notifications/subscriptionsalready available
// Example: Send push notification when user wins
async function notifyWinner(raffleId: number, winnerAddress: string) {
const subscription = await notificationService.getSubscription(
raffleId,
winnerAddress
);
if (subscription?.channel === 'push') {
await pushService.sendToUser(winnerAddress, {
title: 'Congratulations!',
body: `You won raffle #${raffleId}!`,
data: { raffleId: String(raffleId) }
});
}
}Notifications should be triggered by:
-
Raffle End Event
- Listen for raffle finalization on blockchain
- Get all subscribers for the raffle
- Send "raffle ended" notifications
-
Winner Selected Event
- Listen for winner selection on blockchain
- Check if winner is subscribed
- Send "you won" notification
-
Implementation Options
- Blockchain event listener (indexer)
- Scheduled job checking raffle status
- Webhook from contract/oracle
- Authentication: All endpoints require valid JWT
- Authorization: Users can only manage their own subscriptions
- Rate Limiting: Protected by global throttler guard
- Input Validation: DTOs validate all request data
- SQL Injection: Protected by Supabase parameterized queries
- Unique Constraint: Prevents duplicate subscriptions
- Indexes: Optimized for common queries
- Supabase: Handles connection pooling
- Caching: Can add Redis for frequently accessed data
- Batch Operations: Can implement bulk subscribe/unsubscribe
Recommended monitoring:
-
Subscription Metrics
- Total subscriptions
- Subscriptions per raffle
- Active users with subscriptions
-
API Metrics
- Request rate
- Error rate
- Response time
-
Delivery Metrics (future)
- Notifications sent
- Delivery success rate
- Bounce rate
- Email service integration (SendGrid, AWS SES)
- Push notification service (Firebase, OneSignal)
- SMS notifications (Twilio)
- Discord/Telegram webhooks
- Notification preferences (frequency, types)
- Notification history/log
- Batch operations
- Notification templates
- A/B testing for notifications
- Analytics and reporting
- Check JWT token is valid
- Verify raffle_id exists
- Check Supabase connection
- Review server logs
- This is expected behavior
- Frontend should handle gracefully
- Returns existing subscription
- Verify user owns the subscription
- Check raffle_id is correct
- Ensure JWT token matches subscription owner
- Frontend:
client/docs/NOTIFICATIONS.md - Frontend Implementation:
client/NOTIFICATION_IMPLEMENTATION.md - Database Schema:
backend/database/migrations/002_notifications.sql - API Reference: This document
For issues or questions:
- Check server logs:
npm run dev - Verify database migration ran successfully
- Test endpoints with cURL
- Review Supabase dashboard for data