Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ CONFIRMATIONS=12
OPENSEA_API_KEY=
OPENSEA_STREAM_URL=wss://stream.openseabeta.com/socket

# Web Push notifications (optional; generate VAPID keys with `npx web-push generate-vapid-keys`)
WEB_PUSH_VAPID_PUBLIC_KEY=
WEB_PUSH_VAPID_PRIVATE_KEY=
WEB_PUSH_SUBJECT=mailto:noreply@grails.market
WEB_PUSH_TTL_SECONDS=86400

# API
API_PORT=3000
API_HOST=0.0.0.0
Expand Down Expand Up @@ -55,4 +61,4 @@ WEB2_TLD_DATA_API_KEY=
# valuation_settings DB row (cat-admin → Valuation settings), not an env var.

# Node Environment
NODE_ENV=development
NODE_ENV=development
115 changes: 114 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions services/api/migrations/seq/0894_create_push_subscriptions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- Migration: Create push subscriptions
-- Date: 2026-06-21
-- Description: Stores browser Web Push subscriptions for authenticated users

CREATE TABLE IF NOT EXISTS push_subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
expiration_time TIMESTAMP,
device_name VARCHAR(120),
user_agent TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
last_seen_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user
ON push_subscriptions(user_id);

CREATE INDEX IF NOT EXISTS idx_push_subscriptions_enabled_user
ON push_subscriptions(user_id)
WHERE enabled = TRUE;

CREATE INDEX IF NOT EXISTS idx_push_subscriptions_endpoint
ON push_subscriptions(endpoint);

CREATE OR REPLACE FUNCTION update_push_subscriptions_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS update_push_subscriptions_updated_at ON push_subscriptions;
CREATE TRIGGER update_push_subscriptions_updated_at
BEFORE UPDATE ON push_subscriptions
FOR EACH ROW
EXECUTE FUNCTION update_push_subscriptions_updated_at();
5 changes: 4 additions & 1 deletion services/api/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ import { blocksRoutes } from './blocks';
import { commentsRoutes } from './comments';
import { feedRoutes } from './feed';
import { valuationsRoutes } from './valuations';
import { pushRoutes, userPushSubscriptionRoutes } from './push';

export function registerRoutes(fastify: FastifyInstance) {
fastify.register(healthRoutes, { prefix: '/health' });
fastify.register(authRoutes, { prefix: '/api/v1/auth' });
fastify.register(userPushSubscriptionRoutes, { prefix: '/api/v1/users/me/push-subscriptions' });
fastify.register(usersRoutes, { prefix: '/api/v1/users' });
fastify.register(pushRoutes, { prefix: '/api/v1/push' });
fastify.register(verificationRoutes, { prefix: '/api/v1/verification' });
fastify.register(watchlistRoutes, { prefix: '/api/v1/watchlist' });
fastify.register(notificationsRoutes, { prefix: '/api/v1/notifications' });
Expand Down Expand Up @@ -90,4 +93,4 @@ export function registerRoutes(fastify: FastifyInstance) {
fastify.register(commentsRoutes, { prefix: '/api/v1/comments' });
fastify.register(valuationsRoutes, { prefix: '/api/v1/valuations' });
fastify.register(websocketRoutes, { prefix: '/ws' });
}
}
Loading