Add public portal directory at the root domain (multi-tenant) - #1652
Open
diagonraleigh wants to merge 2 commits into
Open
Add public portal directory at the root domain (multi-tenant)#1652diagonraleigh wants to merge 2 commits into
diagonraleigh wants to merge 2 commits into
Conversation
Design for a public portal directory served at the root domain of a multi-tenant instance, so portals on the same instance are discoverable without already knowing their subdomain. Off by default behind PORTAL_DIRECTORY_ENABLED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In multi-tenant mode a portal is only reachable if you already know its subdomain. The root domain resolves no tenant, so RequireTenant returns 404 for every path except /signup, and nothing lists the portals an instance hosts. Serve a browsable directory there instead: every active, non-private portal, each linking to its own URL, with a client-side filter. Off by default behind PORTAL_DIRECTORY_ENABLED, so upgrading an existing instance never starts enumerating its tenants without the operator opting in. - query.GetPublicTenants excludes private, pending, locked and disabled tenants, plus any site inside its deletion grace window. prevent_indexing is deliberately not part of the filter: CreateTenant defaults it to true, so honouring it would leave the directory permanently empty. - RootDomainFallback serves the page where RequireTenant would otherwise 404. It requires the host to be exactly HOST_DOMAIN, so an unknown or disabled subdomain, or a custom domain matching no tenant, keeps its 404 rather than being handed the portal list. - web.TenantLogoURL emits absolute per-portal logo URLs, because /static/images only resolves on a host where a tenant resolves. - The page is registered in public/ssr.tsx, so crawlers receive server-rendered markup instead of the client shell. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue: #1651
Adds a public portal directory at the root domain of a multi-tenant instance, so the portals an instance hosts can be discovered without already knowing their subdomains. Today the root domain resolves no tenant, so
RequireTenant404s every path except/signup.Important
This has not been through the
feedback.fider.io→ roadmap-acceptance flow. PerCONTRIBUTING.mdand discussion #1529 that means you may well close it, and that's a fair outcome — I'm not asking anyone to absorb review burden for an unaccepted feature. It's here so the proposal in #1651 has something concrete attached. Happy to move the idea to feedback.fider.io, cut scope, or close it.Behaviour
Off by default behind a new
PORTAL_DIRECTORY_ENABLEDenv var. With the flag off the root domain 404s exactly as before, so upgrading an instance changes nothing until an operator opts in.Only active, non-private portals are listed. Private, pending, locked and disabled tenants are excluded, as is any site inside its deletion grace window.
/signinNotable decisions
prevent_indexingis deliberately not part of the eligibility filter.CreateTenantinserts it astruefor every new tenant, so honouring it would leave the directory permanently empty. It governs search-engine indexing, not membership of an on-instance list.hostname == HOST_DOMAINcomparison. "No tenant resolved" is not the same as "this is the root domain" — it is equally true of an unknown subdomain, a disabled tenant's subdomain (MultiTenantintentionally leaves the tenant unset there), and a custom domain matching no tenant. Guarding only ontenant == nil && path == "/"turns all of those 404s into a 200 serving the portal list.env.Subdomain(host) == ""is not a valid substitute, since it also returns""for hosts outside the multi-tenant domain."/"route.r.Get("/", handlers.Index())is registered afterRequireTenant, and the router (httprouter) panics on a duplicate method+path.RootDomainFallbacksits immediately beforeRequireTenant— exactly where the root-domain 404 originates — and takes the handler as a parameter somiddlewaresdoesn't importhandlers. Routes registered earlier (/signup,/terms, OAuth) are unaffected, sinceEngine.handlecomposes each chain eagerly at registration time./static/images/*bkeylives in thetenantAssetsgroup behindRequireTenant, so it only resolves on a host where a tenant resolves; a root-domain-relative URL (what the client-sideuploadedImageURLbuilds) would 404. Newweb.TenantLogoURLtakes the tenant explicitly and mirrorsAssetsURL's per-subdomain CDN branch. Portals without a logo render an initial-letter placeholder, since the letter-avatar endpoint is also tenant-gated.public/ssr.tsx. That table is static (esbuild has no dynamic imports) andssrRenderthrows for anything missing from it. SSR is crawler-gated and degrades to the client shell on error, so an omission fails silently — a public directory page crawlers can't read would defeat the point. There's a test asserting a crawler receives real card markup.No database migration. No changes to existing tenant behaviour.
Verification
getPublicTenantseligibility/ordering (3),RootDomainFallbackhost and path guards (5),PortalDirectoryhandler incl. the crawler SSR path (4),web.TenantLogoURLincl. CNAME and CDN (4), the config flag (1), and the page's rendering, filtering and both empty states (7, jest).go test ./... -race -shortgreen other thanapp/services/email/smtp, which already fails at unmodified HEAD (TestSend_Success,TestBatch_Success— verified against a cleangit archive HEADexport; unrelated to this change).