feat(tenant): single-tenant mode core (config SSOT + server/UI guards + branding)

Add lib/tenant.ts as the single source of truth for multi- vs single-tenant
deployment. Gate space creation (server + signup UI), add open-join to the one
space, hide the marketing shell, and route brand name + base URL through config.
Also commit the _migrations_applied table into the canonical schema so a fresh
instance is reproducible from schema.sql alone.

- lib/tenant.ts: resolveTenantConfig (pure) + tenantConfig() + appBaseUrl()
- lib/auth-actions.ts: createSpace refused when !allowSpaceCreation; joinSpace
  open-join resolves the configured/sole space with no invite code
- proxy.ts: marketing paths redirect to /login when showMarketing is off
- app/signup/page.tsx: single-tenant hides create/join chooser, brand=siteName
- app/layout.tsx + 4 link builders: brand/base-URL via config (no hackerspace.sh literal)
- scripts/schema.sql: public._migrations_applied tracking table
- .env.example: documented single-tenant + first-admin bootstrap vars
- package.json: pnpm setup
This commit is contained in:
Moheeb Zara
2026-08-05 12:43:11 -07:00
parent 85f3655e6f
commit 646d2e542c
12 changed files with 301 additions and 29 deletions
+4 -3
View File
@@ -12,6 +12,7 @@
import type { createAdminClient } from '@/lib/supabase/admin'
import { captureException } from '@/lib/observability/capture'
import { appBaseUrl } from '@/lib/tenant'
type AdminClient = ReturnType<typeof createAdminClient>
@@ -122,9 +123,9 @@ export async function getSpaceName(admin: AdminClient, spaceId: string): Promise
// Build the member portal URL the way every enqueue site does. Header host
// takes precedence (lets a request to a non-canonical host link back to
// itself); otherwise falls back to NEXT_PUBLIC_APP_URL and finally the prod
// default. Same shape as the existing dues path.
// itself); otherwise falls back to the configured base URL (appBaseUrl, which
// resolves NEXT_PUBLIC_APP_URL). Same shape as the existing dues path.
export function buildManageUrl(host?: string | null, proto?: string | null): string {
if (host) return `${proto || 'https'}://${host}/me`
return `${process.env.NEXT_PUBLIC_APP_URL || 'https://hackerspace.sh'}/me`
return `${appBaseUrl()}/me`
}