‘Tis the season for gift giving. And, if you’re like me, you enjoy getting gifts for dogs. Atticus (my tri-color corgi) gets his fair share of balls and stuffed toys (that he promptly destroys), but those are all from me. We’ve never participated in a pet-specific holiday gift exchange before, until this year.
While Atticus doesn’t have his own Instagram account, mine is pretty much exclusively Atticus content and we follow many of his corgi friends. In a recent story, his friend Lulu invited fellow dog owners to join a gift exchange. All that’s required is for me to send a gift valued at least $10 to the assigned dog and in return, Atticus could receive anywhere between 6-36 gifts!
I immediately opted in. What do I have to lose? I either send one gift and receive nothing, but make a dog happy, or Atticus receives many gifts, and tons of dogs are happy. Online shopping sites made this very easy.
What wasn’t easy was the gift exchange process. It involved posting to your insta-story and responding to people who wanted to join, manually changing up the gift recipients, and a lot of copy paste. Because there are character limitations to Instagram DMs, the message also had to be split into 3 sends. It was tedious and there was no good way to keep track of your referrals or to have an idea of how many gifts to expect. As a software engineer, I just couldn’t stand for that. So, I built an app.
Gift exchange app overview
The app is fairly straightforward. It needs a referral link that allows people to join the exchange (by giving credit to the person who shared it on their story), and a dashboard to provide further instructions like where to send the gift, their personal referral link, and any tracking information. This can all be accomplished using my preferred stack of Remix, Prisma, CockroachDB Serverless, and Tailwind. But before I could start coding, I had to investigate how this all worked.
I’ve never reverse engineered a social media meme before, but the more I dug into it, the more it looked like some kind of pyramid scheme, but not that bad. Here’s how it works. You send a gift to the person who referred the person that referred you, or your grandreferrer for short. The people you refer send gifts to the person that referred you. Once the people you refer start referring people, then you start receiving gifts. Are you dizzy? Here’s a picture.
You might be thinking, this makes sense, but how does it start? This was the challenge. I’m not sure how this meme began IRL, but here’s the solution I came up with: Because you need a referrer and a grandreferrer, it can’t just start with a single person. To begin a gift exchange you need at least 3 people referring each other to then start referring other people. Trust me, this works out. Here’s another picture.
Gift exchange application architecture
Now that we have all that figured out, let’s build the app. First, we need to decide how we are going to store all the data. We need to take into account authentication, which will be a simple email/password. We’ll need to be able to handle multiple exchanges and store all the metadata information required for each exchange like a mailing address and type of dog. This can be accomplished using the following Prisma models:
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
email String @unique
passwordHash String @map("password_hash")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
Participants Participant[]
Exchanges Exchange[]
@@map("users")
}
model Exchange {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
title String
isArchived Boolean @default(false) @map("is_archived")
ownerId String @map("owner_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
Owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: NoAction)
Participants Participant[]
@@map("exchanges")
}
model Participant {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
address1 String
address2 String?
city String
state String
zip String
country String
dogType String @map("dog_type")
userId String @map("user_id") @db.Uuid
exchangeId String @map("exchange_id") @db.Uuid
referrerId String @map("referrer_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
Exchange Exchange @relation(fields: [exchangeId], references: [id], onDelete: Cascade, onUpdate: NoAction)
Referrer Participant @relation(name: "Referrer", fields: [referrerId], references: [id], onDelete: Cascade, onUpdate: NoAction)
Referrals Participant[] @relation("Referrer")
@@unique([exchangeId, userId])
}
One thing to mention: because we have this circular reference in the Participant model where the referrerId is another Participant, and the referrals is an array of other Participants, we need to disambiguate the relations with the @relation attribute and provide the name argument.
If we set the provider in the schema.prisma file to cockroachdb
and update our DATABASE_URL
environment variable in the .env files with a CockroachDB Serverless connection string, running npx prisma db push
will create the necessary tables in your database and generate the types for the Prisma Client. We’re now ready to create our Remix application.
Application routes
The application consists of the following routes:
/invite/{referrers participantId}
This is a publicly available route that can be sent to people interested in joining the exchange. It includes a summary of the exchange process and a form to sign up and join the specific exchange and set the appropriate referralId. After joining, the user will be redirected to their dashboard with further instructions.
/dashboard/{users participantId}
This is a password-protected route that contains information about exchanges the user is participating in. It includes instructions, information about their gift recipient, their referral link, and details about how many people they’ve referred and the estimated number of gifts they’ll receive.
/
This is the publicly available root of the application. It contains a log-in screen to allow people to return to their dashboard.
Check out the TypeScript files under /app/routes in the project repo.
Creating users
Now we need to create our first 3 users and exchange for them to join. Because this is a small project, I choose to bootstrap it using the Prisma seed file and save creating the new exchange function as a future enhancement. In the /prisma/seed.ts
file, I was able to create 3 users and a new exchange using the Prisma Client.
Creating the Participant records for each user was a little trickier because the referrerId wasn’t available when creating them individually because it was a reference to one of the other’s id which is created at the time insert. To get around this, I used the Prisma $rawQuery
function to request 3 UUIDs from the database and used those as the ids for the Participant records.
const [participantIds] = await db.$queryRaw<
Array<ParticipantIds>
>`SELECT gen_random_uuid() AS "id1", gen_random_uuid() AS "id2", gen_random_uuid() AS "id2" `;
const [participant1, participant2, participant3] = await Promise.all(
[
{
...tmp,
id: participantIds.id1,
userId: user1.id,
referrerId: participantIds.id2,
},
{
...tmp,
id: participantIds.id2,
userId: user2.id,
referrerId: participantIds.id3,
},
{
...tmp,
id: participantIds.id3,
userId: user3.id,
referrerId: participantIds.id1,
},
].map((participant) => {
return db.participant.create({ data: participant });
})
);
Once you add the following to your package.json file, you can run this code using the npx prisma db seed
command to create your first users and exchange.
"prisma": {
"seed": "node --require esbuild-register prisma/seed.ts"
}
Fork the repo
There we have it. A Secret Santa Paws gift exchange made easy. Share your referral URL on your social and wait for the gifts to come in. Not a fan of dogs? (You monster!) Fork the repo, create your own CockroachDB Serverless cluster and adapt it to your liking. I just saw a similar exchange where you send each other Whiskey instead.