Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Roadmap

MOTIR-4209Implemented

(motir-core) The public-address store — a SET of addresses per public project with exactly one primary, retained aliases and the reserved-name set: migration + repository

Opened by Zhu Yue ·

The data layer for public addresses — the Prisma model, its migration and RLS, the reserved-name constant, and a single-operation repository. No service, no route, no UI here: this is the bottom slice of the 4-layer split (motir-core/CLAUDE.md), built exactly to what the ADR decided in Q2, Q3, Q6 and Q7.

What it models — a SET, never a singular

An address is one row in ONE table (PublicAddress, @@map("public_address")), and a project has MANY:

kindnamescardinalitywho owns the row
workspace_subdomain<label>.<base> — the workspace's claimed subdomain; a project answers at /<identifier> under itexactly one live per workspacethe workspace (workspaceId, projectId null)
workspace_subdomain_aliasa previous label after a rename — kept forever, never released, always a redirectN per workspace, capped by the ADR's rename countthe workspace
custom_domaina customer hostname answering for ONE project at its rootN per project, capped by the tier gatethe project (projectId, workspaceId for tenancy)

Fields every row carries: hostname (globally @unique — a hostname resolves to exactly one owner, and an alias holds its name against re-claim by anyone else), kind, status, verificationToken (the _motir-verify TXT value, custom domains only), lastCheckedAt, issuedAt, failureReason, createdAt, updatedAt. The primary is a nullable Project.primaryAddressId (FK → PublicAddress, onDelete: SetNull): null means the ADR's Q6 default rule applies (the subdomain path when one is claimed, else motir.co/p/<identifier>); set means that custom domain is canonical. One FK cannot point at two rows, which is what makes exactly one primary a constraint rather than a convention.

The status enum IS the state checklist (PublicAddressStatus): active (a subdomain that resolves), alias (a retired label), unverified, verifying, pending_certificate, issued, failed, expired, revoked. Every later card — the job, the pane, the design — names what it does per value; add nothing the ADR did not name and drop nothing.

What ships

  • prisma/schema.prisma + the migration: the model, the enum, the FK on Project, indexes on (workspaceId), (projectId), (status, lastCheckedAt) for the status job's sweep. RLS: the table takes the same policy shape project_key_alias carries (read the migration that created it — not the model), and the ANONYMOUS public read the host contract will make must be admissible on the db singleton path findPublicByIdentifier uses; state the policy in the migration's comment.
  • lib/publicAddresses/reservedNames.ts — the reserved-label set from the ADR's Q7 as a ReadonlySet, plus isReservedLabel() and the label grammar (lowercase, digits, hyphen, no leading/trailing hyphen, the ADR's minimum length).
  • lib/repositories/publicAddressRepository.ts — single-Prisma-op methods, required tx on every write: findByHostname, listForProject, listForWorkspace, findLiveSubdomainForWorkspace, createSubdomain(tx), retireSubdomainToAlias(tx) (writes the alias row and the new live row in the caller's transaction), createCustomDomain(tx), updateStatus(tx), setPrimary(tx) (on Project), remove(tx), listByStatusOlderThan(status, before, limit) for the job.
  • Concurrency at the boundary: the unique hostname is the race arbiter — a lost race surfaces as P2002 and the repository rethrows a typed HostnameTakenError (the IdentifierTakenError precedent), never a raw Prisma error.
  • Unit tests: the enum is total in every Record<PublicAddressStatus, …> this card adds; the reserved-name grammar; a real-Postgres repository test for the unique race (two concurrent creates of one hostname → one wins, one HostnameTakenError).

Boundary

No service or route (the subdomain service and the lifecycle own those), no DTO on the public contract (the host card), no entitlement change (the lifecycle card adds the kind). Nothing outside motir-core.

Acceptance criteria

  • The migration creates public_address with a globally unique hostname, the kind and status enums exactly as enumerated above, Project.primaryAddressId nullable with SetNull, and the three indexes; pnpm prisma migrate deploy applies cleanly on an empty database and on one holding existing projects (no data step required — every existing project has zero addresses).
  • The migration carries RLS policies for the table, and a test reads pg_policies for public_address and asserts they exist — a reading of the catalog, not of the migration file.
  • reservedNames.ts exports the ADR's Q7 set verbatim and isReservedLabel rejects every member, single characters, and labels below the minimum length; a test enumerates the set from the ADR's table.
  • Every repository write takes tx; a concurrency test with two real connections proves one HostnameTakenError and one success for the same hostname.
  • No @prisma/client import outside lib/repositories/; no route or component file in the diff.

Context refs

  • the decision — Q2, Q3, Q6, Q7
  • motir-core/prisma/schema.prismaProject (identifier, accessLevel, publicOverviewMd), ProjectKeyAlias (the retained-key precedent and its @@unique), Organization.slug
  • motir-core/lib/repositories/projectRepository.tsfindPublicByIdentifier (the anonymous read path this table must be readable on), listPublic
  • motir-core/lib/repositories/projectKeyAliasRepository.ts — the alias write this mirrors
  • motir-core/CLAUDE.md — the 4-layer split, required-tx writes, P2002 → typed error, the ≥90% per-file floor

Discussion

No comments yet.

Adding to this discussion signs you in on app.motir.co and brings you back to this request.

Add a comment