Skip to content
Open
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
35 changes: 35 additions & 0 deletions ENV_VARS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,64 @@
| STELLAR_NETWORK | Stellar network (testnet or public) | testnet | No |
| OPENAI_API_KEY | OpenAI API key for AI services | - | **Yes** |
| AGENTICPAY_ALLOWED_SIGNATURE_ORIGINS | Allowed origins for EIP-712 signature verification | https://agenticpay.com,http://localhost:3000 | No |
| VAPID_PUBLIC_KEY | VAPID public key for Web Push API | auto-generated | No |
| VAPID_PRIVATE_KEY | VAPID private key for Web Push API | auto-generated | No |
| WS_ENABLED | Enable/disable WebSocket support | true | No |
| WS_PORT | WebSocket port | 3001 | No |

## Frontend

| Variable | Description | Default |
| ----------------------- | -------------------- | ---------------------------- |
| NEXT_PUBLIC_API_URL | Backend API base URL | http://localhost:3001/api/v1 |
| NEXT_PUBLIC_BACKEND_URL | Backend URL fallback | http://localhost:3001/api/v1 |
| NEXT_PUBLIC_WS_URL | WebSocket URL | http://localhost:3001 |
| NEXT_PUBLIC_WS_ENABLED | Enable WebSocket | true |

## Environment Files

- `.env.example`
```
PORT=3001
CORS_ALLOWED_ORIGINS=http://localhost:3000
JOBS_ENABLED=true
STELLAR_NETWORK=testnet
OPENAI_API_KEY=sk-your-openai-api-key
AGENTICPAY_ALLOWED_SIGNATURE_ORIGINS=https://agenticpay.com,http://localhost:3000
VAPID_PUBLIC_KEY=your-vapid-public-key
VAPID_PRIVATE_KEY=your-vapid-private-key
WS_ENABLED=true
```

- `.env.development` — local development
- `.env.staging` — staging environment
- `.env.production` — production environment

## Push Notification Setup

### Generating VAPID Keys

To generate VAPID keys for push notifications:

```bash
cd backend
npm run generate:vapid-keys
```

This will output both public and private keys. Add them to your `.env` file:

```
VAPID_PUBLIC_KEY=<key_from_output>
VAPID_PRIVATE_KEY=<key_from_output>
```

**Important**: Keep your VAPID private key secret and never commit it to version control.

See [PUSH_NOTIFICATIONS.md](./docs/PUSH_NOTIFICATIONS.md) for complete push notification setup guide.

## Notes

- Never commit `.env` files containing real secrets to version control
- Copy the appropriate file and rename to `.env` when running locally
- VAPID keys are required for push notification functionality
- WebSocket support is required for real-time notifications
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"openapi:validate": "node -e \"require('fs').accessSync('docs/api/openapi/openapi.json')\"",
"benchmark": "tsx src/tests/benchmarks/run-benchmarks.ts",
"benchmark:baseline": "tsx src/tests/benchmarks/run-benchmarks.ts --write-baseline",
"benchmark:compare": "tsx src/tests/benchmarks/compare-baseline.ts"
"benchmark:compare": "tsx src/tests/benchmarks/compare-baseline.ts",
"generate:vapid-keys": "node scripts/generate-vapid-keys.js"
},
"dependencies": {
"@prisma/client": "^5.22.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- DropForeignKey
ALTER TABLE "notification_logs" DROP CONSTRAINT "notification_logs_subscription_id_fkey";

-- DropTable
DROP TABLE "notification_logs";

-- DropTable
DROP TABLE "push_preferences";

-- DropTable
DROP TABLE "push_subscriptions";

-- DropEnum
DROP TYPE "NotificationStatus";

-- DropEnum
DROP TYPE "NotificationCategory";
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
-- CreateEnum
CREATE TYPE "NotificationCategory" AS ENUM (
'payment_notification',
'dispute_alert',
'project_update',
'milestone_reminder',
'security_alert',
'subscription_update',
'system_notification'
);

-- CreateEnum
CREATE TYPE "NotificationStatus" AS ENUM (
'pending',
'sent',
'delivered',
'clicked',
'failed'
);

-- CreateTable "push_subscriptions"
CREATE TABLE "push_subscriptions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"endpoint" TEXT NOT NULL,
"auth" TEXT NOT NULL,
"p256dh" TEXT NOT NULL,
"user_agent" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"last_used_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),

CONSTRAINT "push_subscriptions_pkey" PRIMARY KEY ("id")
);

-- CreateTable "push_preferences"
CREATE TABLE "push_preferences" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"payment_notifications" BOOLEAN NOT NULL DEFAULT true,
"dispute_alerts" BOOLEAN NOT NULL DEFAULT true,
"project_updates" BOOLEAN NOT NULL DEFAULT true,
"milestone_reminders" BOOLEAN NOT NULL DEFAULT true,
"security_alerts" BOOLEAN NOT NULL DEFAULT true,
"subscription_updates" BOOLEAN NOT NULL DEFAULT true,
"system_notifications" BOOLEAN NOT NULL DEFAULT true,
"group_notifications" BOOLEAN NOT NULL DEFAULT true,
"notify_sound" BOOLEAN NOT NULL DEFAULT true,
"notify_badge" BOOLEAN NOT NULL DEFAULT true,
"locale" TEXT NOT NULL DEFAULT 'en',
"timezone" TEXT NOT NULL DEFAULT 'UTC',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,

CONSTRAINT "push_preferences_pkey" PRIMARY KEY ("id")
);

-- CreateTable "notification_logs"
CREATE TABLE "notification_logs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"subscription_id" TEXT,
"category" "NotificationCategory" NOT NULL,
"status" "NotificationStatus" NOT NULL DEFAULT 'pending',
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"icon" TEXT,
"badge" TEXT,
"tag" TEXT,
"data" JSONB,
"deep_link" TEXT,
"sent_at" TIMESTAMP(3),
"delivered_at" TIMESTAMP(3),
"clicked_at" TIMESTAMP(3),
"error" TEXT,
"retry_count" INTEGER NOT NULL DEFAULT 0,
"metadata" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,

CONSTRAINT "notification_logs_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "push_subscriptions_tenant_id_user_id_endpoint_key" ON "push_subscriptions"("tenant_id", "user_id", "endpoint");

-- CreateIndex
CREATE INDEX "push_subscriptions_tenant_id_user_id_idx" ON "push_subscriptions"("tenant_id", "user_id");

-- CreateIndex
CREATE INDEX "push_subscriptions_endpoint_idx" ON "push_subscriptions"("endpoint");

-- CreateIndex
CREATE INDEX "push_subscriptions_is_active_idx" ON "push_subscriptions"("is_active");

-- CreateIndex
CREATE INDEX "push_subscriptions_created_at_idx" ON "push_subscriptions"("created_at");

-- CreateIndex
CREATE UNIQUE INDEX "push_preferences_tenant_id_user_id_key" ON "push_preferences"("tenant_id", "user_id");

-- CreateIndex
CREATE INDEX "push_preferences_tenant_id_idx" ON "push_preferences"("tenant_id");

-- CreateIndex
CREATE INDEX "push_preferences_user_id_idx" ON "push_preferences"("user_id");

-- CreateIndex
CREATE INDEX "notification_logs_tenant_id_user_id_idx" ON "notification_logs"("tenant_id", "user_id");

-- CreateIndex
CREATE INDEX "notification_logs_status_idx" ON "notification_logs"("status");

-- CreateIndex
CREATE INDEX "notification_logs_category_idx" ON "notification_logs"("category");

-- CreateIndex
CREATE INDEX "notification_logs_subscription_id_idx" ON "notification_logs"("subscription_id");

-- CreateIndex
CREATE INDEX "notification_logs_sent_at_idx" ON "notification_logs"("sent_at");

-- CreateIndex
CREATE INDEX "notification_logs_tag_idx" ON "notification_logs"("tag");

-- AddForeignKey
ALTER TABLE "notification_logs" ADD CONSTRAINT "notification_logs_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "push_subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
104 changes: 104 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,24 @@ enum EmailStatus {
failed
}

enum NotificationCategory {
payment_notification
dispute_alert
project_update
milestone_reminder
security_alert
subscription_update
system_notification
}

enum NotificationStatus {
pending
sent
delivered
clicked
failed
}

enum DeliveryProvider {
smtp
sendgrid
Expand Down Expand Up @@ -479,3 +497,89 @@ model EmailAnalytics {
@@index([date])
@@map("email_analytics")
}

// ─── Push Notification Models ──────────────────────────────────────────────────

model PushSubscription {
id String @id @default(uuid())
tenantId String @map("tenant_id")
userId String @map("user_id")
endpoint String
auth String // VAPID auth secret
p256dh String // VAPID public key
userAgent String? @map("user_agent")
isActive Boolean @default(true) @map("is_active")
lastUsedAt DateTime? @map("last_used_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")

notifications NotificationLog[]

@@unique([tenantId, userId, endpoint])
@@index([tenantId, userId])
@@index([endpoint])
@@index([isActive])
@@index([createdAt])
@@map("push_subscriptions")
}

model PushPreference {
id String @id @default(uuid())
tenantId String @map("tenant_id")
userId String @map("user_id")
paymentNotifications Boolean @default(true) @map("payment_notifications")
disputeAlerts Boolean @default(true) @map("dispute_alerts")
projectUpdates Boolean @default(true) @map("project_updates")
milestoneReminders Boolean @default(true) @map("milestone_reminders")
securityAlerts Boolean @default(true) @map("security_alerts")
subscriptionUpdates Boolean @default(true) @map("subscription_updates")
systemNotifications Boolean @default(true) @map("system_notifications")
groupNotifications Boolean @default(true) @map("group_notifications")
notifySound Boolean @default(true) @map("notify_sound")
notifyBadge Boolean @default(true) @map("notify_badge")
locale String @default("en")
timezone String @default("UTC")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([tenantId, userId])
@@index([tenantId])
@@index([userId])
@@map("push_preferences")
}

model NotificationLog {
id String @id @default(uuid())
tenantId String @map("tenant_id")
userId String @map("user_id")
subscriptionId String? @map("subscription_id")
category NotificationCategory
status NotificationStatus @default(pending)
title String
body String
icon String?
badge String?
tag String? // For grouping notifications
data Json? // Custom data for deep linking
deepLink String? @map("deep_link")
sentAt DateTime? @map("sent_at")
deliveredAt DateTime? @map("delivered_at")
clickedAt DateTime? @map("clicked_at")
error String?
retryCount Int @default(0) @map("retry_count")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

subscription PushSubscription? @relation(fields: [subscriptionId], references: [id])

@@index([tenantId, userId])
@@index([status])
@@index([category])
@@index([subscriptionId])
@@index([sentAt])
@@index([tag])
@@map("notification_logs")
}

55 changes: 55 additions & 0 deletions backend/scripts/generate-vapid-keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env node

/**
* Script to generate VAPID keys for Web Push API
* Usage: node scripts/generate-vapid-keys.js
*/

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

function urlBase64Encode(buffer) {
return buffer
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

function generateVapidKeys() {
const curve = crypto.createECDH('prime256v1');
curve.generateKeys();

const publicKey = urlBase64Encode(curve.getPublicKey());
const privateKey = urlBase64Encode(curve.getPrivateKey());

return { publicKey, privateKey };
}

try {
console.log('🔑 Generating VAPID keys for Web Push API...\n');

const keys = generateVapidKeys();

console.log('✅ VAPID keys generated successfully!\n');
console.log('📋 Add the following to your .env file:\n');
console.log(`VAPID_PUBLIC_KEY=${keys.publicKey}`);
console.log(`VAPID_PRIVATE_KEY=${keys.privateKey}\n`);

console.log('⚠️ Important Security Notes:');
console.log(' - Keep your VAPID_PRIVATE_KEY secret');
console.log(' - Never commit .env to version control');
console.log(' - Store keys in secure environment variables\n');

// Optionally create or update .env.local
const envPath = path.join(__dirname, '..', '.env');
if (process.argv.includes('--update-env') && !fs.existsSync(envPath)) {
const envContent = `VAPID_PUBLIC_KEY=${keys.publicKey}\nVAPID_PRIVATE_KEY=${keys.privateKey}\n`;
fs.writeFileSync(envPath, envContent);
console.log(`✅ Created ${envPath} with VAPID keys`);
}
} catch (error) {
console.error('❌ Error generating VAPID keys:', error.message);
process.exit(1);
}
Loading