Skip to content

Data model ​

One Postgres database holds accounts, workspaces, links, and event data.

Masir requires Postgres 18 because primary keys use the native uuidv7() function. Drizzle defines the schema and generates forward-only migrations.

Relationship map ​

Tables ​

TablePurpose
usersAccount identity, verification, and session version
auth_identitiesPassword, Google, and Microsoft sign-in methods
user_tokensSingle-use verification and password-reset tokens
workspacesTenant name, slug, plan, link prefix, logo, and state
workspace_membersUser role and active state inside a workspace
workspace_invitationsOpen, accepted, and revoked invitations
campaignsShared campaign and medium values
linksDestination, slug, access rules, targeting, counters, and state
link_importsCSV import batches keyed by workspace and file hash
link_aliasesCurrent and revoked extra slugs
tagsWorkspace tag names
link_tagsLink-to-tag relationships
hostsDeduplicated referrer host names
click_eventsPartitioned redirect events
link_daily_statsReserved daily aggregate shape
audit_eventsProduct and security change history
mail_outboxDatabase-backed mail outbox shape

Tenant boundary ​

User-reachable product rows carry a workspace ID. Repository operations accept the workspace as input before they accept a row ID. This prevents a valid row ID from crossing a workspace boundary.

Click events keep workspace and link IDs without foreign keys. An event log must not block or cascade a product deletion.

Identity and sessions ​

users.session_version lets the server invalidate every sealed cookie for an account. Password reset increments the value.

Password hashes live on auth_identities, not on the user row. One user can connect several providers. The last identity cannot be removed.

user_tokens stores SHA-256 hashes for email verification and password reset. Expired rows are removed during database preparation.

Workspace constraints ​

workspace_members uses (workspace_id, user_id) as its primary key. A partial unique index permits one owner per workspace.

Only one open invitation can exist for one workspace and email pair.

Important columns include:

ColumnMeaning
slugPublic address, unique in the workspace
destination_urlCurrent HTTP or HTTPS destination
is_enabledImmediate off switch
starts_at, expires_atAvailability window
scheduled_destinationFallback before opening
expiration_destinationFallback after expiry
limit_destinationFallback after the visit cap
password_hashOptional Argon2id hash
maximum_visitsOptional successful-human-visit cap
click_countAtomic successful human redirect count
targetingCountry and operating-system destinations
notesPrivate workspace text
utm_mediumOptional link-level medium that overrides the campaign value
import_id, import_rowOptional CSV import identity; unique together
responsible_user_idOptional member who follows up; set null on user delete or member removal
review_atOptional date when the link should be checked again
archived_atHidden from default lists; redirects still work
deleted_atSoft-delete marker

Status is derived at read time. A scheduled link becomes active without a job.

Deleted links keep their slug. This stops an old public address from later pointing to a different link.

Aliases ​

link_aliases uses (workspace_id, slug) as its primary key. A revoked alias remains reserved. A link can have 10 active aliases.

Resolution checks the primary slug, then an active alias.

Campaigns and tags ​

A link can belong to one campaign and many tags.

A database check prevents a campaign link from also setting its own utm_campaign. The campaign remains the single source for that value.

Click events ​

One event records the request outcome, daily visitor hash, referrer host, country, device, browser, bot class, and time.

Columns include:

ColumnMeaning
workspace_id, link_idScoping IDs without foreign keys
campaign_idEffective campaign ID snapshot
outcomeSmallint numeric outcome code
device, browserInteger classification codes
bot_category, is_botBot detection attributes
countryTwo-letter ISO country code
referrer_hostForeign key to hosts.id
visitor_hashDaily salted visitor hash
attribution_versionAttribution format version (1 for current attribution, null on blocks or legacy events)
utm_sourceEffective UTM source snapshot (at most 120 characters)
utm_mediumEffective UTM medium snapshot (at most 120 characters)
utm_campaignEffective UTM campaign snapshot (at most 120 characters)
utm_contentEffective UTM content snapshot (at most 120 characters)

It does not store a raw IP address or full user-agent string. It does not record utm_term or other query values.

The visitor hash is a salted daily bigint. It supports daily unique counts for one link and cannot join a visitor across days.

Events use monthly range partitions. Database preparation creates the current month and the next two. A daily job repeats this, so a long-running instance always has a partition ready. Retention can drop a partition instead of deleting rows one at a time.

Audit events ​

Audit rows contain a type, optional actor, optional workspace and link, JSON detail, and time.

Events without a workspace, such as sign-in failures and abuse reports, do not appear in a workspace activity feed.

Migrations ​

Migrations live under drizzle/. Generate and inspect them after a schema change:

sh
bun run db:generate
bun run db:migrate

At boot, Masir applies pending migrations under an advisory lock, prepares click partitions, and removes expired tokens.

Open source link management for teams. Released under the MIT License.