Private
Public Access
1
0

Auto-create tenant for any hostname that reaches the app

DNS is the gatekeeper — if a request arrives, it was pointed
here intentionally. Every new hostname gets its own tenant
automatically, no manual setup or seeding required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-25 22:30:05 -05:00
parent 7b540a37ee
commit 80c17d4c6f
2 changed files with 67 additions and 30 deletions
+10 -21
View File
@@ -6,28 +6,19 @@ const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Create default tenant
const tenant = await prisma.tenant.upsert({
where: { slug: 'default' },
update: {},
create: {
name: 'Default Workspace',
// The app auto-creates a tenant on first request,
// but the seed ensures an admin user exists.
let tenant = await prisma.tenant.findFirst();
if (!tenant) {
tenant = await prisma.tenant.create({
data: {
name: 'Pounce',
slug: 'default'
}
});
// Map hostnames to default tenant
const hostnames = ['localhost', '127.0.0.1', 'pounce.catrenelle.com'];
for (const hostname of hostnames) {
await prisma.tenantHostname.upsert({
where: { hostname },
update: {},
create: {
hostname,
tenantId: tenant.id,
isPrimary: hostname === 'localhost'
}
});
console.log(` Created tenant: ${tenant.name} (${tenant.id})`);
} else {
console.log(` Existing tenant: ${tenant.name} (${tenant.id})`);
}
// Create admin user
@@ -46,8 +37,6 @@ async function main() {
});
console.log('Seed complete.');
console.log(` Tenant: ${tenant.name} (${tenant.id})`);
console.log(` Hostnames: ${hostnames.join(', ')}`);
console.log(' Admin: admin@example.com / admin123');
}
+51 -3
View File
@@ -19,21 +19,69 @@ export async function resolveTenant(hostname: string): Promise<TenantInfo | null
return cached.tenant;
}
// Look up existing hostname mapping
const record = await prisma.tenantHostname.findUnique({
where: { hostname: host },
include: { tenant: true }
});
if (!record) return null;
if (record) {
const tenant: TenantInfo = {
id: record.tenant.id,
name: record.tenant.name,
slug: record.tenant.slug
};
tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL });
return tenant;
}
// No mapping found — auto-provision
// If a default tenant exists, map this hostname to it.
// If no tenants exist at all, create the first one.
const tenant = await autoProvisionTenant(host);
if (tenant) {
tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL });
}
return tenant;
}
async function autoProvisionTenant(host: string): Promise<TenantInfo> {
// Any hostname that reaches this app was pointed here via DNS,
// so it's intentional. Auto-create a tenant for it.
const slug = slugify(host);
const name = humanizeDomain(host);
const tenant = await prisma.tenant.create({
data: {
name,
slug,
hostnames: {
create: { hostname: host, isPrimary: true }
}
}
});
return { id: tenant.id, name: tenant.name, slug: tenant.slug };
}
function slugify(host: string): string {
return host
.replace(/[^a-zA-Z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.toLowerCase()
.slice(0, 48);
}
function humanizeDomain(host: string): string {
// "pounce.catrenelle.com" -> "Pounce"
// "192.168.3.65" -> "Pounce"
// "localhost" -> "Pounce"
const part = host.split('.')[0];
if (/^\d+$/.test(part) || part === 'localhost') {
return 'Pounce';
}
return part.charAt(0).toUpperCase() + part.slice(1);
}
export function clearTenantCache() {