Bifröst
Bifröst
Super admin
General admin
Employee
Request leave
Notifications Mark all read
Loading…
·
Esc
Dashboard Calendar Request leave My leave Employment info Policy Report a bug
Admin
People Requests Grants & PTO Salaries Rules & VR Reporting Access
System
System guide Bug reports

System guide & onboarding

How Bifröst works, how to run it, and why it's safesuper
On this page
What Bifröst isHow it's builtSecurity & privacyRoles & accessVacation & the monthly ledgerSick & child-sickHow a leave request flowsCarry-over, illness, terminationWhat runs automaticallyNotifications & emailsVR rules (versioned)The pages, one by oneSetting it up (checklist)Where data lives

What Bifröst is

Bifröst is Myrkur's internal HR & leave manager. It replaced VacationTracker because that tool couldn't encode Icelandic VR union rules — the way vacation (orlof) and sick leave (veikindaréttur) accrue and step up with tenure. Everything here is built around that one hard requirement.

It does four things: tracks who's off (calendar), lets people request leave and managers approve it, computes each person's balances from union rules, and quietly flags patterns worth a human's attention.

The guiding idea: balances are never stored, they're derived. Every figure you see is recomputed from an append-only history of events and a set of versioned rules. That's what makes it auditable and safe to change rules later.

How it's built

There is no application server. The site is static (Astro) served from bifrost.myrkur.is, and the browser talks directly to the database (Supabase / PostgreSQL) over its API. That sounds alarming until you understand the security model below — it's deliberate and it's how the rest of Myrkur's internal tools are built.

  • Frontend — static pages. They hold no secrets beyond a public "anon" key that can do nothing on its own.
  • Database — PostgreSQL with Row-Level Security (RLS). Every table decides, per row, who may read or write it. This is the real security boundary.
  • Sensitive logic — balances, entitlements, kennitala, anomaly scores — is computed inside the database by trusted functions, never assembled in the browser.
  • Scheduled jobs run inside the database on a timer (accrual posting, sick scans, carry-over). They are writers, not the source of truth.
  • Email is sent straight from the database to Resend over HTTPS.

Security & privacy

Login is Google, restricted to @myrkur.is. There are no passwords in Bifröst to leak. A non-Myrkur Google account is rejected at the door. Signing in gives the browser a short-lived token that identifies the person on every request.

RLS is the only thing that matters. Because the browser talks to the database directly, we assume the frontend can be inspected and the anon key is public — none of that helps an attacker, because every query is filtered by database policies tied to the logged-in identity. A normal employee simply cannot read another person's request, kennitala, or balance, no matter what they type.

The PII / medical split. "Manager" is a notification relationship, not a permission tier. The shared calendar shows who is off to everyone and the leave category — for medical that's "Sick leave" or "Child sick" — but never the reason, the note, or any diagnosis: those stay with the person who filed. What's coarsened is the detail, not the fact of being off. Kennitala, date of birth and address are visible only to super admins.

Two admin levels. General admins run the day-to-day (approvals, people, grants). Super admins additionally see sensitive fields and control rules, access, and carry-over. The distinction is enforced in the database, not just hidden in the UI.

Rule of thumb: if a screen shows something sensitive, that data was computed and filtered server-side by a trusted function. The browser is never trusted to keep a secret.

Roles & access

  • Employee — sees their own balances, files leave, views the team calendar (coarsened), reads policy.
  • General admin — everything above, plus People, the approval queue, Grants & PTO, Rules, and Reporting. Cannot see kennitala/DOB or approve their own leave.
  • Super admin — everything, plus sensitive personal fields, the Access page (who is an admin), carry-over decisions, and this guide. Super admins may approve their own leave.

A super admin can preview the app as a general admin or an employee using the "View" switch in the top bar — useful for checking what staff actually see. The switch itself never disappears while previewing, because it's tied to who you really are, not the role you're viewing.

Vacation & the monthly ledger

Vacation (orlof) accrues monthly. How much depends on the person's VR tier, which is decided by tenure. The tier is the highest one whose thresholds the person meets on that date — company months, company years, years in the profession, or age. Tiers currently run from 24 up to 30 days per year. The engine recomputes the tier every month from the person's tenure on that date, so a step-up happens automatically the month someone crosses a threshold.

How a month's accrual is computed. The engine takes the annual entitlement for that month's tier, spreads it across the year by actual days, and scales by the person's working fraction (FTE):

month's days = annual entitlement × (accruable days in the month ÷ 365.25) × FTE
  • A full-time employee on the 24-day base tier accrues about 2.0 days/month (24 × 30.4 ÷ 365.25 ≈ 2.0). On the 30-day tier it's ~2.5.
  • Part-time scales it — someone at 50% FTE accrues half. FTE comes from their effective-dated employment segments, so a change in hours takes effect from its date forward.
  • Partial months prorate by day — a mid-month start, an opening-balance "as of" date, or a termination only counts the days actually inside the working period.
  • Parental or unpaid leave pauses accrual for the days it covers, so time fully off doesn't build vacation — unless an admin ticks "keep accruing" when registering it.
  • The rate is read at the month's end, so the month you cross a tier already accrues at the higher number.

The current (unfinished) month accrues live day by day using this same formula; when the month closes it's frozen into the ledger exactly as computed.

The saved monthly states (the ledger). This is the most important internal concept. When a month completes, the accrual for that month is frozen — stored with the exact rules, FTE and tenure that applied then. A person's balance is:

Balance = sum of all frozen past months  +  the live current month  −  leave taken.

Because past months are frozen, changing a rule, an FTE, or a start date only moves the current and future months — it never silently rewrites someone's history. You can adjust a person's contract in good conscience knowing last year's numbers won't shift under you. Each employee's frozen months are visible on their record ("Vacation accrual" — month by month, marked frozen or in progress).

Opening balances. Migrated staff start with a vacation figure carried from the old system and a "balance as of" date; accrual runs forward from there. New hires start at zero and accrue from their start date.

Sick & child-sick

Sick leave is never pre-accrued — it's a rolling 12-month entitlement. A sick day counts against the allocation and ages out exactly 12 months later. Sick and child-sick each have their own allocation, scaled by tenure and FTE.

Filing sick leave auto-approves as long as it's within allocation — no manager step, because it's a right, not a request. (Two exceptions become a review: going over allocation, and falling ill during booked vacation — see below.) Migrated staff can carry an "already used in the last 12 months" figure so the rolling window is accurate from day one.

How a leave request flows

  • File — from "Request leave". The form checks policy live (short notice, over balance, etc.) and shows warnings; a flagged request can still be filed.
  • Notify — the person's manager gets an in-app notification and an email. If no manager is set (an oversight), it falls back to the super admins so the request is never missed — and that person shows a "No manager" flag on the People page.
  • Decide — an admin approves or rejects in the queue. A rejection requires a reason, which the employee sees by email, in their notification, and permanently on their My Leave and record.
  • Appear — an approved leave shows on the team calendar by category only (medical shows as "Sick leave" / "Child sick", never the reason) and counts against the balance.

Self-approval: a general admin cannot decide their own request — it must go to another admin. Super admins may decide their own. This is enforced in the database.

The queue vs. history: the Requests page has an actionable pending queue at the top and a decision history log below. An approved request lives in the history — it is not "still in the queue". The sidebar Requests badge counts only genuinely pending items and updates live.

Cancellation: an employee can cancel a pending request, or an already-approved future one; if it was approved, the approver is notified.

Revoking an approved leave: a super admin can withdraw an already-approved leave from the decision history (e.g. someone's called into work). It asks for a reason — shown to the employee like a decline reason, for transparency — credits their days back automatically, and notifies them. Reversible: they can re-file.

Carry-over, illness & termination

Year-end carry-over (1 May). At the holiday-year boundary, leftover vacation over the 20-day cap is flagged for a super admin to decide how many days carry across. Everyone keeps their full balance until that decision is made. Super admins are exempt — their carry-over completes automatically.

Illness during vacation. If someone falls ill while on booked vacation, they file a sick day as normal. Because it overlaps approved vacation, it does not auto-approve — it goes to review flagged "Sick during booked vacation". On approval the system automatically converts those days from vacation back to sick: the vacation is credited back, the sick is booked. No doctor's note lives in the system; that stays a manager's-discretion conversation.

Termination. Setting a termination date on a person's record freezes their accrual on that day (the final month prorates to it; nothing accrues after) and shows the settlement — the remaining vacation days to pay out, or the days they owe back if they over-took. It also deactivates the record. Payout itself is payroll's job.

What runs automatically

These jobs run on a timer inside the database — no one has to trigger them:

  • Monthly accrual posting — on the 1st, freezes the just-finished month's vacation accrual for every active employee into the ledger.
  • Daily sick scan — looks for concerning patterns (Bradford score, Friday/Monday clustering, spikes) and files any it finds under Reporting → "Alerts to action", emailing the person's manager.
  • Daily tier scan — spots when someone's tenure crosses into a higher vacation tier and emails both them and their manager, stating the reason (company years or profession years).
  • Year-end carry-over — at the 1 May boundary, computes leftover days and opens the carry-over review.
  • Holiday refresh — keeps Icelandic public holidays computed years ahead, so working-day maths is always correct.

Notifications & emails

Every email is also an in-app notification (the bell, top-right, updates live). Email is sent from Bifröst <bifrost@notify.myrkur.is> via Resend, on a dedicated subdomain so the main Myrkur mail is never touched. All of it is no-reply — the footer says so, and nothing arrives back.

To the employee: leave approved, leave declined (with the reason), a vacation grant or correction, a carry-over decision, and a tenure tier increase.

To the manager: a new leave request to review, a sick-pattern flag, a cancelled approved leave, and a report that one of their people crossed into a higher tier. If a person has no manager, the new-request and sick-alert emails fall back to the super admins. Reassigning someone's manager forwards their still-pending requests to the new manager, so a hand-over doesn't drop anything.

Mechanically: an action writes a notification row → a database trigger renders the branded email → it's sent to Resend over HTTPS. The Resend key lives in a locked table only the sending function can read.

VR rules (versioned)

The union figures — vacation tiers, sick allocations, wage steps — are stored as effective-dated data, not hard-coded. Each has a "valid from" date, so a rule change is a single edit on the Rules page, no code change. Because past accrual is frozen in the ledger, a new rule version only affects the current and future months; you can even schedule a change ahead of time or view any prior version's numbers. Only super admins can edit rules.

The pages, one by one

  • Dashboard — your own vacation (as a ring) and sick balances, upcoming leave, who's off this week.
  • Calendar — the whole team, month by month, filterable by leave type; medical shows its category ("Sick leave" / "Child sick") but no reason or note.
  • Request leave — the leave form with live policy checks.
  • My leave — every request you've filed with its status, your notes, decline reasons, and your balance-change history.
  • Employment info / Policy — your own employment details and the written orlof/veikindaréttur policy.
  • People (admin) — the roster (Directory) and everyone's balances month-by-month (Balances). Click through to a full record: profile, FTE, manager, opening balances, the accrual ledger, full leave history, termination.
  • Requests (admin) — the approval queue, carry-over review (super), and decision history.
  • Grants & PTO (admin) — one-off or permanent vacation grants; a negative value deducts/corrects.
  • Rules & VR (admin) — the versioned union rules.
  • Reporting (admin) — sick anomaly dashboard and the "Alerts to action".
  • Access (super) — who is an admin, and deactivating former staff.

Press Ctrl/Cmd K anywhere for the command palette to jump to a page or person.

Setting it up (checklist)

To take Bifröst live for real staff:

  • 1. Add each employee — name, email, start date, profession-seniority date, contract, and FTE. The tenure dates drive the whole vacation engine, so get them right.
  • 2. Enter opening balances — for anyone carrying over from the old system: their current vacation days, the "as of" date, and any sick already used in the last 12 months.
  • 3. Assign managers — this is what turns on the manager-routed notifications and emails. Until set, those simply don't fire.
  • 4. Confirm the rules on Rules & VR match current VR figures.
  • 5. Set who's an admin on Access.

You can set people up before they ever sign in. Use + Add person on the People page to create a record from just a name + @myrkur.is email, then fill in their manager, opening balances and tenure ahead of time. Leaving the name blank is fine — their Google name fills it in on first sign-in; if you type a name, yours wins. Pre-created people carry a "Not signed in" marker until they log in.

Sign-in links automatically — and nobody is emailed early. When a person signs in with Google, their account attaches to the pre-created record by email (case-insensitive): everything you set up is kept, with no duplicate. And crucially, no email is ever sent to someone who hasn't logged in yet — anything you do during setup (grants, registered leave, and so on) is recorded as an in-app notification they'll see when they arrive, but emails only begin once they've actually signed in. So you can prepare everyone quietly and go live without a burst of premature emails.

The engine, emails, calendar and automation are already running. Onboarding is almost entirely entering real people and their tenure — everything else follows from that.

Where data lives

Everything is in the Supabase Postgres database — identities, requests, the frozen accrual ledger, rules, notifications, and the audit log. The database is backed up by Supabase. Because history is append-only (the ledger and audit log are never overwritten), the system can always explain why a number is what it is: which rule, which month, who decided. Corrections are made by posting new entries, never by editing the past.

Any attachments or HR documents (for example a performance note on someone's record) sit in a private storage bucket keyed to the employee — reachable only by that person and admins. Note that the illness-during-vacation flow deliberately stores no doctor's note; that stays a manager conversation.

This page is visible to super admins only. Keep it in step with the system as it evolves.

Are you sure?