diff --git a/apps/web/prisma/migrations/20260226120000_add_public_ids/migration.sql b/apps/web/prisma/migrations/20260226120000_add_public_ids/migration.sql
new file mode 100644
index 00000000..316bb1fd
--- /dev/null
+++ b/apps/web/prisma/migrations/20260226120000_add_public_ids/migration.sql
@@ -0,0 +1,23 @@
+-- AlterTable
+ALTER TABLE "ApiKey" ADD COLUMN "publicId" TEXT;
+
+-- AlterTable
+ALTER TABLE "Domain" ADD COLUMN "publicId" TEXT;
+
+-- AlterTable
+ALTER TABLE "Team" ADD COLUMN "publicId" TEXT;
+
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "publicId" TEXT;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiKey_publicId_key" ON "ApiKey"("publicId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Domain_publicId_key" ON "Domain"("publicId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Team_publicId_key" ON "Team"("publicId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_publicId_key" ON "User"("publicId");
diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma
index 9ef292b6..2d60df24 100644
--- a/apps/web/prisma/schema.prisma
+++ b/apps/web/prisma/schema.prisma
@@ -80,6 +80,7 @@ model VerificationToken {
model User {
id Int @id @default(autoincrement())
+ publicId String? @unique
name String?
email String? @unique
emailVerified DateTime?
@@ -100,6 +101,7 @@ enum Plan {
model Team {
id Int @id @default(autoincrement())
+ publicId String? @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -181,6 +183,7 @@ enum DomainStatus {
model Domain {
id Int @id @default(autoincrement())
+ publicId String? @unique
name String @unique
teamId Int
status DomainStatus @default(PENDING)
@@ -209,6 +212,7 @@ enum ApiPermission {
model ApiKey {
id Int @id @default(autoincrement())
+ publicId String? @unique
clientId String @unique
tokenHash String
partialToken String
diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
index be4691b9..c7d03d39 100644
--- a/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
+++ b/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
@@ -42,7 +42,7 @@ export default function DomainItemPage({
const domainQuery = api.domain.getDomain.useQuery(
{
- id: Number(domainId),
+ id: domainId,
},
{
refetchInterval: (q) => (q?.state.data?.isVerifying ? 10000 : false),
@@ -54,7 +54,7 @@ export default function DomainItemPage({
const handleVerify = () => {
verifyQuery.mutate(
- { id: Number(domainId) },
+ { id: domainId },
{
onSettled: () => {
domainQuery.refetch();
@@ -154,7 +154,9 @@ export default function DomainItemPage({
/>
hello,
useSend is the best open source sending platform
check out usesend.com",
});
- }
+ },
),
});
diff --git a/apps/web/src/server/api/routers/template.ts b/apps/web/src/server/api/routers/template.ts
index 6e90e686..f2d9a710 100644
--- a/apps/web/src/server/api/routers/template.ts
+++ b/apps/web/src/server/api/routers/template.ts
@@ -13,6 +13,7 @@ import {
getDocumentUploadUrl,
isStorageConfigured,
} from "~/server/service/storage-service";
+import { newId } from "~/server/id";
export const templateRouter = createTRPCRouter({
getTemplates: teamProcedure
@@ -64,6 +65,7 @@ export const templateRouter = createTRPCRouter({
.mutation(async ({ ctx: { db, team }, input }) => {
const template = await db.template.create({
data: {
+ id: newId("template"),
...input,
teamId: team.id,
},
@@ -134,6 +136,7 @@ export const templateRouter = createTRPCRouter({
async ({ ctx: { db, team, template }, input }) => {
const newTemplate = await db.template.create({
data: {
+ id: newId("template"),
name: `${template.name} (Copy)`,
subject: template.subject,
content: template.content,
diff --git a/apps/web/src/server/api/trpc.ts b/apps/web/src/server/api/trpc.ts
index 5f741b11..14a51356 100644
--- a/apps/web/src/server/api/trpc.ts
+++ b/apps/web/src/server/api/trpc.ts
@@ -16,6 +16,7 @@ import { getServerAuthSession } from "~/server/auth";
import { db } from "~/server/db";
import { getChildLogger, logger, withLogger } from "../logger/log";
import { randomUUID } from "crypto";
+import { parseNumericId } from "~/server/id";
/**
* 1. CONTEXT
@@ -147,7 +148,7 @@ export const teamProcedure = protectedProcedure.use(async ({ ctx, next }) => {
session: { ...ctx.session, user: ctx.session.user },
},
});
- }
+ },
);
});
@@ -163,11 +164,30 @@ export const teamAdminProcedure = teamProcedure.use(async ({ ctx, next }) => {
});
export const domainProcedure = teamProcedure
- .input(z.object({ id: z.number() }))
+ .input(
+ z.object({
+ id: z.union([z.number().int().positive(), z.string().min(1)]),
+ }),
+ )
.use(async ({ ctx, next, input }) => {
- const domain = await db.domain.findUnique({
- where: { id: input.id, teamId: ctx.team.id },
- });
+ const domainId =
+ typeof input.id === "number" ? input.id : parseNumericId(input.id);
+ const domainPublicId = typeof input.id === "string" ? input.id : null;
+
+ const domain =
+ domainId !== null
+ ? await db.domain.findUnique({
+ where: { id: domainId, teamId: ctx.team.id },
+ })
+ : domainPublicId
+ ? await db.domain.findFirst({
+ where: {
+ publicId: domainPublicId,
+ teamId: ctx.team.id,
+ },
+ })
+ : null;
+
if (!domain) {
throw new TRPCError({ code: "NOT_FOUND", message: "Domain not found" });
}
@@ -205,7 +225,7 @@ export const contactBookProcedure = teamProcedure
.input(
z.object({
contactBookId: z.string(),
- })
+ }),
)
.use(async ({ ctx, next, input }) => {
const contactBook = await db.contactBook.findUnique({
@@ -225,7 +245,7 @@ export const campaignProcedure = teamProcedure
.input(
z.object({
campaignId: z.string(),
- })
+ }),
)
.use(async ({ ctx, next, input }) => {
const campaign = await db.campaign.findUnique({
@@ -245,7 +265,7 @@ export const templateProcedure = teamProcedure
.input(
z.object({
templateId: z.string(),
- })
+ }),
)
.use(async ({ ctx, next, input }) => {
const template = await db.template.findUnique({
diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts
index 1f53f4c5..5adedbdb 100644
--- a/apps/web/src/server/auth.ts
+++ b/apps/web/src/server/auth.ts
@@ -13,6 +13,7 @@ import { Provider } from "next-auth/providers/index";
import { sendSignUpEmail } from "~/server/mailer";
import { env } from "~/env";
import { db } from "~/server/db";
+import { newId } from "~/server/id";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -60,7 +61,7 @@ function getProviders() {
scope: "read:user user:email",
},
},
- })
+ }),
);
}
@@ -70,7 +71,7 @@ function getProviders() {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
allowDangerousEmailAccountLinking: true,
- })
+ }),
);
}
@@ -84,7 +85,7 @@ function getProviders() {
async generateVerificationToken() {
return Math.random().toString(36).substring(2, 7).toLowerCase();
},
- })
+ }),
);
}
@@ -120,6 +121,7 @@ export const authOptions: NextAuthOptions = {
events: {
createUser: async ({ user }) => {
let invitesAvailable = false;
+ const publicId = newId("userPublic");
if (user.email) {
const invites = await db.teamInvite.findMany({
@@ -136,12 +138,12 @@ export const authOptions: NextAuthOptions = {
) {
await db.user.update({
where: { id: user.id },
- data: { isBetaUser: true },
+ data: { isBetaUser: true, publicId },
});
} else {
await db.user.update({
where: { id: user.id },
- data: { isBetaUser: true, isWaitlisted: true },
+ data: { isBetaUser: true, isWaitlisted: true, publicId },
});
}
},
diff --git a/apps/web/src/server/id.ts b/apps/web/src/server/id.ts
new file mode 100644
index 00000000..3f68895f
--- /dev/null
+++ b/apps/web/src/server/id.ts
@@ -0,0 +1,47 @@
+import { customAlphabet } from "nanoid";
+
+const ID_ALPHABET =
+ "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+const ID_SUFFIX_LENGTH = 16;
+
+const nextIdSuffix = customAlphabet(ID_ALPHABET, ID_SUFFIX_LENGTH);
+
+export const ID_PREFIX = {
+ userPublic: "usr",
+ teamPublic: "tm",
+ domainPublic: "dom",
+ apiKeyPublic: "ak",
+ sesSetting: "ses",
+ teamInvite: "inv",
+ email: "em",
+ emailEvent: "evt",
+ contactBook: "cb",
+ contact: "ct",
+ campaign: "cmp",
+ template: "tpl",
+ suppression: "sup",
+ webhook: "wh",
+ webhookCall: "call",
+} as const;
+
+export type IdKind = keyof typeof ID_PREFIX;
+type PrefixFor