Back to all projects

2026 · Solo developer

Isinboard

A personal dashboard for ISINs. Paste certificates, stocks, ETFs and bonds, get official EU reference data, and price them with your own provider keys.

  • typescript
  • nextjs
  • react
  • supabase
  • vitest
Isinboard cover image

I follow a handful of certificates, bonds and ETFs, and every one of them lives on a different page of a different site. An ISIN is the only thing they have in common. I wanted a single board where I paste a list of ISINs and see, for each one, what it is, who issued it, when it matures and what it trades at.

Every ISIN, one board: cards, layouts and a certificate's details. All instruments in the video are fictitious.

What it is

A Next.js web app, deployed on Vercel with Supabase behind it. You add one or many ISINs, and each one becomes a card with its name, type, issuer, venue, maturity, underlyings and, when a price source is connected, its last price and a one-year chart. You can search and filter by type, market, issuer, underlying, currency and maturity, split ISINs across pages, and share a page read-only behind a password.

LayerTechnology
AppNext.js App Router, React 19, server actions
StorageSupabase (Postgres with RLS), or a local JSON file in development
Reference dataESMA FIRDS, GLEIF, OpenFIGI
PricesBring-your-own-key adapters: iTick, EODHD, plus a personal-use source
SecurityHMAC-signed session cookies, AES-256-GCM for stored API keys, sign-in throttling
QualityTypeScript strict, Vitest, ESLint

Adding ISINs from anything you can paste

People keep ISINs in spreadsheets, broker exports and emails, so the add dialog accepts any of those. Any run of characters that isn’t a letter or a digit separates tokens, and only ISIN-shaped tokens are kept:

export function parseIsinList(text: string): ParsedIsinList {
  const tokens = text.toUpperCase().split(/[^A-Z0-9]+/).filter(Boolean);
  for (const token of tokens) {
    if (!ISIN_PATTERN.test(token)) ignored++;           // headers, names, prices, dates
    else if (!isValidIsin(token)) invalid.push(token);  // right shape, wrong check digit
    else if (valid.includes(token)) duplicates.push(token);
    else valid.push(token);
  }
  ...
}

The split between “ignored” and “invalid” matters. A column header or a price is noise and is dropped silently. A code with the right shape but a wrong check digit is almost certainly a typo, so it is reported back to the user. The check digit is a Luhn sum over the first eleven characters, with letters expanded to 10–35.

Reference data from public registers

The descriptive part of every card comes from ESMA FIRDS, the EU register of financial instruments. From one record Isinboard reads the name, the CFI code (which tells a certificate from a bond, an ETF or a stock), the venue, the first and last trading dates, the maturity, the nominal value, the coupon and the underlyings. GLEIF turns the issuer’s and underlyings’ LEI codes into legal names. Both are free and public, and neither needs a key.

FIRDS doesn’t say whether a certificate is still alive, so Isinboard estimates it. The phase comes from the trading dates and the last price:

if (info.tradingStart > today) return estimate("subscription", info.tradingStart);
if (end && end < today) {
  const early = info.maturity !== null && daysBetween(end, info.maturity) > EARLY_GAP_DAYS;
  return early ? estimate("endedEarly", end) : estimate("expired", info.maturity ?? end);
}
if (end && daysBetween(today, end) <= LAST_DAYS) return estimate("lastDays", end);

A venue stops trading a few days before maturity, so a longer gap means the certificate was called early. A listed instrument with no new price for a week is flagged as possibly redeemed or suspended. Because the register records the end of trading but not the reason, the UI always presents the phase as an estimate, with the reasoning in a tooltip.

Prices: bring your own key

Isinboard doesn’t resell market data. Each user connects their own provider accounts, and the keys are stored encrypted with AES-256-GCM and only ever decrypted on the server. Sources are tried in the user’s order, and the first one that can price an ISIN wins.

Free plans are tight (EODHD allows 20 requests a day, iTick 5 a minute), so the price service is mostly budgeting:

  • Every call is counted against both limits. A per-minute and a per-day rate limiter are checked before any request. When the provider reports the account’s real daily limit, that number replaces the free-plan default.
  • Symbol searches are paid once. Mapping an ISIN to a provider’s ticker costs a request, so the result is stored. A “not listed” answer is retried after a week. Searches get at most half of the remaining budget, so prices for ISINs that are already mapped keep flowing.
  • The stalest quotes go first. Within the budget, quotes are refreshed oldest-first. End-of-day quotes are also persisted, so a cold serverless start doesn’t spend the daily quota again.
  • Caches are per user. A provider’s terms cover one account holder’s own use, so a quote fetched with one user’s key is never shown to another user.

Fields and layouts that need data none of the user’s sources provide, such as the chart without a history-capable source, are shown disabled with an explanation instead of being hidden.

Views, pages and sharing

  • Global rule, per-ISIN override. A grid of one to three columns or a compact list. Each card uses one of four layouts (Compact, Details, Split, Chart) and a chosen set of fields. A rule set on a single ISIN wins over the global one, layout and fields independently.
  • Read-only sharing. A page can be shared with a link and a password that expire within seven days. Guests get a separately signed cookie, see quotes only for that page’s ISINs, and only from the sources the owner picked. A source marked personal-use is never offered to guests.
  • English and Italian. en.ts is the typed source of truth, and every other dictionary is typed against it, so a missing key fails the build. Server code returns error codes and the client translates them.
  • A demo mode. npm run demo starts a separate build with 32 fictitious instruments (every ISIN contains DEMO), simulated prices and a year of history. Nothing reaches an external API. It is what the video above shows.

What I’d do next

  • Multiple users. Every store call already takes a user id and every table has row-level security. What’s missing is OAuth sign-in in place of the single user configured from environment variables.
  • Alerts. A barrier or a coupon date on a certificate is exactly the kind of event I want to hear about, rather than spot on the board.
  • More licensed price sources, especially one that covers certificates on SeDeX and EuroTLX, which today only the personal-use source prices.