Santa Paws: Gift exchange sample app for dogs

Santa Paws: Gift exchange sample app for dogs

Netflix, Twitter, DoorDash...

Learn how big tech builds with CockroachDB

Watch Talks

‘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.

Secret Santa Dog Toy Exchange

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 logistics

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.

User dashboard

/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.

gifts received

/

This is the publicly available root of the application. It contains a log-in screen to allow people to return to their dashboard.

log in screen

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.

Keep Reading

How to build a serverless polling application

Very few traditions can melt the corporate ice as well as an ugly holiday sweater contest. Which is why, this past …

Read more
How to build a serverless polling application

Very few traditions can melt the corporate ice as well as an ugly holiday sweater contest. Which is why, this past …

Read more
Full text search with CockroachDB and Elasticsearch

Full text indexing and search is such a common feature of applications these days. Users expect to be able to find a …

Read more