What You'll Build
A trademark portfolio tracker that does three things: imports your trademarks from Signa's database of 147M+ records across 200+ offices, surfaces renewal deadlines sorted by urgency, and pushes real-time alerts when a trademark's status changes. The entire backend fits in two Supabase tables, one Edge Function, and a handful of Signa API calls.
The architecture splits responsibilities cleanly. Supabase handles authentication, data storage, and the webhook receiver. Signa handles everything trademark-specific: searching and retrieving marks, computing deadlines (renewal windows, grace periods, urgency levels), and monitoring portfolios for changes. Your application code sits between them, translating trademark intelligence into a UI your team can act on.
This separation matters. Trademark deadlines are surprisingly complex. A US registration requires a Section 8 declaration of use (a filing that proves the mark is still in active commercial use) between years 5 and 6 after registration, then renewals every 10 years, each with its own filing window and grace period. Signa's deadline engine computes all of this automatically, so you don't have to encode jurisdiction-specific rules yourself.
By the end of this tutorial, you'll have a working tracker you can extend into a full trademark portfolio management tool.
Prerequisites:
- A Supabase project (free tier works)
- A Signa API key
- Node.js 18+
Set Up the Project
Install both SDKs:
npm install @signa-so/sdk @supabase/supabase-js
Create two tables in Supabase. portfolios groups trademarks by organization or project. tracked_marks stores each trademark's Signa ID alongside the metadata you need for display and filtering.
create table portfolios (
id uuid primary key default gen_random_uuid(),
name text not null,
created_at timestamptz default now()
);
create table tracked_marks (
id uuid primary key default gen_random_uuid(),
portfolio_id uuid references portfolios(id) on delete cascade,
signa_trademark_id text not null unique,
mark_text text not null,
status text,
nice_classes int[],
jurisdiction text,
next_deadline_date date,
next_deadline_type text,
updated_at timestamptz default now()
);
The schema is intentionally minimal. Signa is the source of truth for trademark data. Store only what you need for queries, sorting, and display. When a user drills into a specific mark, fetch the full record from Signa in real time.
Set up your environment variables:
# .env
SIGNA_API_KEY=sig_your_api_key_here
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Initialize both clients:
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
That's the foundation for your Supabase trademark tracking app: two SDK clients, two tables, three environment variables.
Import Trademarks into Your Portfolio
The import flow has three steps: search for a trademark by name, retrieve the full record, and upsert it into Supabase.
Start with a search. Signa supports multiple matching strategies in a single call. Running exact and phonetic together catches both the precise mark and similar-sounding variants (think "ACME" matching "AKME"). For a deeper look at search strategies, see Build a Brand Name Availability Checker.
const searchResults = await signa.trademarks.search({
query: 'NORTHWIND',
strategies: ['exact', 'phonetic']
});
console.log(`Found ${searchResults.data.length} results`);
The equivalent curl:
curl -G "https://api.signa.so/v1/trademarks/search" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "query=NORTHWIND" \
--data-urlencode "strategies=exact,phonetic"
Once the user selects the correct mark from the search results, retrieve the full record. The retrieve call returns everything: registration details, owner information, Nice classes (the international system for categorizing goods and services, where Class 9 covers software and Class 25 covers clothing), and the computed deadlines array.
const trademark = await signa.trademarks.retrieve('tm_abc123');
// The deadlines array contains every upcoming filing obligation
console.log(trademark.deadlines);
// [
// {
// type: 'renewal',
// name: 'Trademark Renewal',
// due_date: '2027-03-15',
// window_opens: '2026-09-15',
// status: 'future',
// urgency: 'routine',
// days_until_due: 231
// },
// {
// type: 'declaration_of_use',
// name: 'Section 8 — Declaration of Use',
// due_date: '2026-11-02',
// window_opens: '2026-05-02',
// grace_expiry: '2027-05-02',
// status: 'window_open',
// urgency: 'upcoming',
// days_until_due: 97
// }
// ]
Wrap the full flow into an import function that searches, retrieves, and writes to Supabase:
async function importTrademark(portfolioId: string, signaTrademarkId: string) {
const trademark = await signa.trademarks.retrieve(signaTrademarkId);
const nextDeadline = trademark.deadlines
?.sort((a, b) => new Date(a.due_date).getTime() - new Date(b.due_date).getTime())
?.[0];
const { error } = await supabase.from('tracked_marks').upsert({
portfolio_id: portfolioId,
signa_trademark_id: trademark.id,
mark_text: trademark.mark_text,
status: trademark.status.stage,
nice_classes: trademark.classifications?.map(c => c.nice_class),
jurisdiction: trademark.office_code,
next_deadline_date: nextDeadline?.due_date ?? null,
next_deadline_type: nextDeadline?.type ?? null,
updated_at: new Date().toISOString()
}, { onConflict: 'signa_trademark_id' });
if (error) throw error;
return trademark;
}
The upsert on signa_trademark_id ensures re-importing a mark updates the existing row rather than creating a duplicate. The next_deadline_date column lets you sort trademarks by urgency directly in Supabase queries.
Build the Portfolio Tracker Dashboard
Deadlines are the core of trademark portfolio management. A missed renewal means losing the registration. A missed Section 8 declaration means the same thing. The difference between a healthy portfolio and a crisis is visibility into what's due and when.
Signa's trademarks.list method accepts a renewal_due_before filter that returns only marks with deadlines falling before a given date. Combined with sort: 'renewal_due_date', you get a prioritized list with the most urgent items first. For the full picture on renewal timelines, fees, and consequences, see Trademark Renewal: Deadlines, Fees, and What Happens If You Miss One.
const threeMonthsOut = new Date();
threeMonthsOut.setMonth(threeMonthsOut.getMonth() + 3);
const upcoming = await signa.trademarks.list({
renewal_due_before: threeMonthsOut.toISOString().split('T')[0],
sort: 'renewal_due_date'
});
curl -G "https://api.signa.so/v1/trademarks" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "renewal_due_before=2026-10-28" \
--data-urlencode "sort=renewal_due_date"
Each trademark in the response includes a deadlines array. Every deadline object contains fields that tell you exactly where things stand:
type: The kind of deadline (renewal,declaration_of_use,combined_renewal_and_use, etc.)due_date: The actual filing deadlinewindow_opens: When the filing window opens (you can file early for renewals)grace_expiry: The end of the grace period, if applicablestatus: One offuture,window_open,due_soon,in_grace, ormissedurgency: A computed level (routine,upcoming,critical,in_grace,overdue,missed) based on proximity and statusdays_until_due: Integer countdown (negative when overdue)consequence_if_missed: Plain-language explanation of what happens if this deadline passes without action
The status field is particularly useful for building a dashboard. A mark in future status needs no action yet. A mark in window_open can be filed at the filer's convenience. due_soon means the deadline is approaching. in_grace means the original deadline has passed but a late filing (typically with additional fees) is still possible. missed means the window has closed entirely.
Group deadlines by urgency to build a triage view:
type Urgency = 'critical' | 'overdue' | 'in_grace' | 'upcoming' | 'routine' | 'missed';
interface TrademarkWithDeadlines {
id: string;
mark_text: string;
office_code: string;
deadlines?: {
type: string;
due_date: string;
days_until_due: number;
status: string;
urgency: Urgency;
consequence_if_missed?: string;
}[];
}
function groupByUrgency(trademarks: TrademarkWithDeadlines[]) {
const groups: Record<Urgency, any[]> = {
critical: [],
overdue: [],
in_grace: [],
upcoming: [],
routine: [],
missed: []
};
for (const tm of trademarks) {
for (const deadline of tm.deadlines ?? []) {
groups[deadline.urgency]?.push({
mark_text: tm.mark_text,
office_code: tm.office_code,
deadline_type: deadline.type,
due_date: deadline.due_date,
days_until_due: deadline.days_until_due,
status: deadline.status,
consequence: deadline.consequence_if_missed
});
}
}
return groups;
}
const grouped = groupByUrgency(upcoming.data);
console.log(`Critical: ${grouped.critical.length}`);
console.log(`Overdue: ${grouped.overdue.length}`);
This gives you a clean data structure for rendering. Critical items go to the top of the dashboard, with the consequence_if_missed text displayed as a warning. The deadline engine handles all the jurisdiction-specific logic (different offices have different grace periods, different renewal cycles) so your frontend code stays simple.
You can also sync these deadlines back to Supabase for offline querying and Supabase Realtime subscriptions:
for (const tm of upcoming.data) {
const nextDeadline = tm.deadlines?.[0];
if (nextDeadline) {
await supabase.from('tracked_marks').update({
next_deadline_date: nextDeadline.due_date,
next_deadline_type: nextDeadline.type,
updated_at: new Date().toISOString()
}).eq('signa_trademark_id', tm.id);
}
}
Monitor Your Portfolio with Watches
A deadline dashboard is reactive. Watches turn it into a trademark monitoring dashboard that's proactive. Instead of polling for changes, you tell Signa which trademarks to monitor and receive webhook notifications when something happens: a status change, an updated record, or a new filing that affects your portfolio.
First, register a webhook endpoint. Webhooks in Signa are account-scoped (one endpoint receives alerts from all watches). The API returns a signing secret once at creation time, so store it immediately.
const webhook = await signa.webhooks.create({
url: `${process.env.SUPABASE_URL}/functions/v1/signa-webhook`,
description: 'Portfolio tracker alerts',
enabled_events: ['alert.created']
});
// Store webhook.secret — it's only returned once
Create a portfolio watch. The watch_type: 'portfolio' variant monitors specific trademarks by ID. For a broader look at monitoring strategies, including opposition deadline tracking, see Automate Opposition Deadline Tracking.
curl -X POST "https://api.signa.so/v1/watches" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Northwind Portfolio",
"watch_type": "portfolio",
"query": {
"version": "v2",
"filters": {
"trademarkIds": ["tm_abc123", "tm_def456", "tm_ghi789"]
}
}
}'
The SDK equivalent:
const watch = await signa.watches.create({
name: 'Northwind Portfolio',
watch_type: 'portfolio',
query: {
version: 'v2',
filters: {
trademarkIds: ['tm_abc123', 'tm_def456', 'tm_ghi789']
}
}
});
console.log(`Watch created: ${watch.id}`);
By default, a portfolio watch triggers on three event types: trademark.created (a new filing appears that matches your filters), trademark.updated (any field on a tracked mark changes), and trademark.status_changed (the registration status moves, for example from published to registered or from registered to expired).
Set up a Supabase Edge Function to receive these webhooks. Create supabase/functions/signa-webhook/index.ts:
const WEBHOOK_SECRET = Deno.env.get('SIGNA_WEBHOOK_SECRET')!;
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
function verifyWebhook(body: string, headers: Headers): boolean {
const msgId = headers.get('webhook-id');
const timestamp = headers.get('webhook-timestamp');
const signatures = headers.get('webhook-signature');
if (!msgId || !timestamp || !signatures) return false;
const content = `${msgId}.${timestamp}.${body}`;
const expected = createHmac('sha256', Buffer.from(WEBHOOK_SECRET, 'base64'))
.update(content)
.digest('base64');
return signatures.split(' ').some(sig => {
const value = sig.replace('v1,', '');
return timingSafeEqual(Buffer.from(expected), Buffer.from(value));
});
}
Deno.serve(async (req) => {
const body = await req.text();
if (!verifyWebhook(body, req.headers)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(body);
const { data } = payload;
const { error } = await supabase.from('tracked_marks').update({
status: data.trademark.status.stage,
next_deadline_date: data.deadline?.must_act_by?.split('T')[0] ?? null,
updated_at: new Date().toISOString()
}).eq('signa_trademark_id', data.trademark.id);
if (error) {
console.error('Failed to update tracked mark:', error);
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
}
console.log(`Processed ${data.event.type} for ${data.trademark.mark_text}`);
return new Response(JSON.stringify({ received: true }), { status: 200 });
});
A typical alert payload for a status change:
{
"type": "alert.created",
"data": {
"id": "alt_abc123",
"object": "alert",
"watch": {
"id": "wat_xyz789",
"name": "Northwind Portfolio",
"type": "portfolio"
},
"event": {
"type": "trademark.status_changed",
"summary": "Status changed from published to registered"
},
"trademark": {
"id": "tm_abc123",
"mark_text": "NORTHWIND",
"office_code": "US",
"status": { "primary": "active", "stage": "registered" },
"nice_classes": [9, 42]
},
"deadline": {
"severity": "routine",
"must_act_by": "2032-07-28T00:00:00Z"
},
"timestamps": {
"occurred_at": "2026-07-28T14:30:00Z",
"created_at": "2026-07-28T14:30:05Z"
}
}
}
In this example, the mark's status changed to registered, meaning it passed through the opposition period (the 30-day window where anyone can file a challenge to block registration) without objection. The alert payload includes watch.id so you know which watch triggered it, the trademark's current status, and deadline severity information.
Deploy the Edge Function with supabase functions deploy signa-webhook and the watch starts delivering events immediately. Every status change, every updated record flows into your Supabase database and can trigger Supabase Realtime subscriptions in your frontend.
For more context on working with trademark data structures and what each field means, see the Developer's Guide to Trademark Data.
Extending the Tracker
The portfolio tracker you've built handles the fundamentals: import, deadlines, and monitoring. A few directions to extend it.
Similarity watches. Beyond monitoring your own trademarks, you can create watches that alert you when someone files a mark that's confusingly similar to yours. This uses a different watch_type with phonetic and fuzzy matching strategies. Catching a conflicting filing early (during the opposition period) is significantly cheaper than challenging it after registration.
Notification routing. The Edge Function currently updates a database row. Add logic to route alerts based on urgency: critical deadlines go to Slack or email, routine updates stay in the dashboard. Different stakeholders care about different events.
Batch sync. For larger portfolios, schedule a periodic job that calls signa.trademarks.list with your tracked IDs and reconciles the full dataset. Webhooks handle real-time changes; batch sync catches anything a webhook might miss during downtime. If you're building for e-commerce, see Trademark Checks for E-Commerce Platforms for patterns that apply to high-volume portfolio management.
Consult a trademark attorney for legal guidance on filing decisions. The tracker surfaces the data. The decisions still require professional judgment.
Sign up for a free Signa API key at signa.so to start building your own trademark portfolio tracker.
