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
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
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 2,
"plugins": []
}
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