Invite activation URL — field migration guide (plan-155 → plan-156)
What this guide covers
Plan 155 (PR svc-access#98) fixed bug D1: the activation_url field in invitation responses was only populated when a devMode flag was set, leaving production operators without a usable link. The fix made activation_url unconditionally populated and introduced a legacy alias _dev_link for backward compatibility.
This guide describes the current transition state and the steps to complete it in plan-156.
Current state (post–plan-155)
Send invite — POST /access/admin/tenants/{id}/invite
The backend (svc-access) returns both fields, populated identically:
{
"invitation": { … },
"activation_url": "https://access.eigenoid.com/activate?token=abc123",
"_dev_link": "https://access.eigenoid.com/activate?token=abc123"
}
| Field | Status |
|---|---|
activation_url | Canonical — new name since plan-155. Migrate reads here. |
_dev_link | Legacy alias — kept for app-access-admin backward compat. Remove after frontend migrates. |
app-access-admin (src/lib/services/invitations.ts) currently reads _dev_link via the legacyInviteField indirection constant. This is intentional until plan-156 updates the reads.
Regenerate invite — POST /access/admin/tenants/{id}/users/{uid}/invite/regenerate
Returns a separate DTO — no migration needed:
{
"invite_url": "https://access.eigenoid.com/activate?token=xyz789",
"expires_at": "2026-08-01T12:00:00Z"
}
invite_url is the stable field for this endpoint. It is not the same as _dev_link or activation_url — it comes from a different response type (InviteRegenerateResult).
Bug D1 — what was fixed
Before plan-155, InvitationService.Create and RegenerateToken accepted a devMode bool parameter. When devMode was false (the production default), neither _dev_link nor activation_url was populated in the response. Admins attempting to copy an invite link in production received an empty field.
Plan-155 removed the devMode gate and removed config.IsDevLinkVisible(). The activation URL is now always constructed via internal/urlutil.BuildActivationURL(PUBLIC_BASE_URL, token) and returned unconditionally.
Plan-156 completion checklist
Complete these steps to finish the migration and remove the legacy field:
1. Update app-access-admin
In src/lib/services/invitations.ts:
- const legacyInviteField = ["_dev", "link"].join("_");
…
return {
invitation: raw.invitation as Invitation,
- invite_url: raw[legacyInviteField] as string | undefined,
+ invite_url: raw["activation_url"] as string | undefined,
};
Update the InvitationResult JSDoc and remove the legacyInviteField constant.
2. Remove DevLinkLegacy from svc-access
In internal/service/invitation_service.go:
type InvitationResult struct {
Invitation *model.Invitation `json:"invitation"`
ActivationURL string `json:"activation_url"`
- // DevLinkLegacy mirrors ActivationURL under the legacy field name "_dev_link"
- // TODO(plan-156): remove after frontend migrates to activation_url.
- DevLinkLegacy string `json:"_dev_link,omitempty"`
}
Also remove the DevLinkLegacy population in Create and RegenerateToken.
3. Verify no remaining _dev_link reads
# In app-access-admin repo — should return zero results after step 1:
grep -r "_dev_link\|legacyInviteField\|dev.*link" src/
4. Smoke-test in dev
- Create a new invitation via the admin portal → copy the activation URL from the success dialog.
- Open the activation URL in an incognito browser tab — the enrollment flow should start.
- Regenerate an invite for a
pending_inviteuser → confirm the URL in the dialog is valid.
Cache-Control: no-store
Both invite endpoints set Cache-Control: no-store. This header is intentional — activation tokens are single-use credentials and must not be stored by shared caches or CDN edges. Do not remove it.
References
svc-accessPR: eigenoid/svc-access#98svc-accessCHANGELOG entry: Plan 155- Source:
internal/urlutil/activation.go—BuildActivationURL - Source:
internal/service/invitation_service.go—InvitationResult - Source:
app-access-admin/src/lib/services/invitations.ts—legacyInviteField