DEV Community

Cover image for The Dog Community: A Home for Dog Owners, Where the Resident Know-It-All Is a Golden Retriever Running on Gemini
Aditya Chavan
Aditya Chavan

Posted on

The Dog Community: A Home for Dog Owners, Where the Resident Know-It-All Is a Golden Retriever Running on Gemini

DEV Weekend Challenge: Dog Days Edition Submission πŸ•

This is a submission for Weekend Challenge: Dog Days Edition

What I Built

The Dog Community β€” a home on the internet for people who own dogs, built
around a member called Barkley.

Barkley is a golden retriever. He has a user row, a bcrypt password hash nobody
holds, an avatar, a card in the community's dog rail alongside everyone else's
pets, and a direct-message thread you can open like you'd open one with any
other member. Nothing in the interface marks him out. There is no chat panel,
no floating button in the corner, no "Ask AI" box. He is in the room, and he
behaves like someone who is in the room β€” which is a very different design
problem from putting a model behind a text field.

I started from something I think is genuinely underserved. Dog owners have
nowhere that is theirs. They have general-purpose social networks where a
photo of their dog competes with politics, and they have breed forums that
feel like 2006. What they actually want is a place where the whole point is
dogs: where you can post a picture of yours and people care, where you can ask
why your four-month-old puppy has started shredding the sofa and get an answer
from three people who have lived through it, where you can find the other
owners near you and end up walking together on Sunday. Real friendships,
real meetups, real trips β€” the kind of thing that starts because two people
found out their dogs are the same breed and both terrible on a lead.

So the app is built for that, not for chatting in the abstract:

  • Three channels that already know what they're for. #general, where the pack hangs out. #puppy-training β€” chewed furniture, shared wisdom. And #breed-talk, whose topic line is literally "Ask Barkley, he has opinions." The community arrives with somewhere to go.
  • The pack rail β€” a running gallery of every dog in the community, down the side of the app at all times. Each card is a photo, a name, a breed, and the owner it belongs to. You are never talking to usernames. You are talking to Priya, who owns Maple, who is a corgi. That single piece of context changes the register of a whole conversation.
  • Photos everywhere, with captions. Pick an image, it uploads, it attaches to your draft so you can say something about it, and it lands in every open window. Channels and DMs both.
  • Direct messages between any two members, so the community can go private when it needs to β€” swapping a vet's number, arranging to meet.
  • Breeds that nobody types. You add your dog by giving a name and a photo. The breed is worked out from the picture. There is no dropdown of 400 breeds and no chance to get your own dog's breed wrong in front of everyone.

And running through all of it, Barkley.

Spotlight: Barkley

He deserves his own section, because he is the reason this is a community and
not a message board.

He greets every new dog personally, and by breed. The moment you add your
dog, Barkley sees the photo β€” the actual photo, not a description of it β€” and
welcomes them into #general by name, with something specific about that
breed that shows he knows what he's talking about. A beagle gets told about
its nose. A corgi gets told about the herding. It's the first thing that
happens to a new member, and it lands in the main channel where everyone sees
it, which means the new arrival gets a welcome from the room too.

Every single thing he says has a dog in it. The persona is fixed: warm,
enthusiastic, a bit smug about how much he knows, and every so often derailed
by the fact that he is a dog. One to three sentences, plain text, no bullet
points, no "As an AI." He never breaks character and he never explains what
he is. The result is that a serious answer about crate training arrives with a
joke about the crate, and the mood of the channel goes up rather than flat.
That is not decoration β€” it is the difference between a member and a lookup.

Tag him and he answers immediately. @barkley anywhere, in any channel, in
any DM, and he replies to what was just said. A typing indicator appears with
his name on it while he thinks, exactly as it would for a person.

DM him and he's always there. Open a direct message with Barkley and he
answers every message β€” no tagging, no prefix, no command. It's a private
conversation with the one member who is always awake. This is where the
embarrassing questions go, and where a new owner at 2am asks the thing they
wouldn't ask the whole channel.

He interrupts you when it matters. This is the part I care about most.
Barkley watches for a set of things that genuinely hurt dogs β€” chocolate,
grapes, raisins, xylitol, ibuprofen, onions β€” and if one of them shows up in a
message, he cuts into the conversation uninvited. Nobody tagged him.
Nobody asked. Someone mentions giving the dog a bit of their chocolate brownie
and Barkley is suddenly in the channel, alarmed, explaining theobromine in his
own words. Every one of those substances is stored with the fact he must not
get wrong, and that fact goes into the prompt β€” so the warning is written
fresh in his voice every time, but the pharmacology underneath it is fixed and
correct. It cannot drift, and it never reads like a canned string.

He does the same thing to pictures. Post an uncaptioned photo of a bowl of
grapes with no text at all and he'll still warn you, because he looked at it.
There is nothing in the words that could have triggered that.

And β€” this is deliberate β€” he shuts up the rest of the time. Plain
conversation between members gets silence. He is not in your chat. He is in
the room, and he speaks when he's spoken to, when a photo needs comment, or
when something dangerous is on the table. A member who answers everything
isn't a member; he's a search box wearing a face. Restraint is the whole
design, and it's enforced in one function.

The effect is that the community feels hosted. Someone knowledgeable and
good-natured is always present, welcomes every arrival, keeps the mood up, and
steps in when a dog might get hurt.

Demo

A full walkthrough of the community β€” adding a dog and watching a breed appear
that nobody typed, Barkley welcoming it by name, an unprompted interruption
about chocolate, a mention, and a direct message:

The community, and the pack rail:

The #general channel, with the dog rail on the right

#breed-talk β€” where Barkley does most of the talking:

#breed-talk

A direct message with him β€” no tag needed, he answers everything:

A direct message with Barkley

And a photo shared in a DM:

Sharing a photo in a direct message

Every word Barkley says in those screenshots was generated by the model
against the live app. None of it is written by me.

Code

The Dog Community

A small chat community for dog owners, with one unusual member: Barkley a golden retriever who lives in the database as a real user row and knows every dog in the room.

Upload a photo of your dog and the app works out the breed for you. Barkley notices the new arrival, welcomes it by name with something breed-specific and quietly interrupts if anyone mentions feeding their dog chocolate.

The #general channel, with the dog rail on the right

What it does

  • Photos everywhere β€” pick an image and it uploads, attaches to the draft so it can carry a caption, and lands in every open window over the same socket as text. Channels and DMs both.
  • Channels and DMs over one WebSocket β€” send and delivery share a single connection, so a message is in both windows before the sender's spinner would have finished.
  • Photo in, breed out β€” adding a dog runs one vision…

How I Built It

FastAPI, React with Vite, PostgreSQL, one WebSocket, and Gemini through Google
AI Studio doing both the seeing and the speaking.

Four decisions I'd defend in a review:

Barkley is a row, not a special case. He is seeded on boot as a users
row with is_bot = true and a dogs row of his own. Every consequence falls
out for free: he appears in the member list, so he appears in the DM picker,
so DMing him works without a line of code written for it. His card in the pack
rail is a real query result, not a hardcoded component. The only thing in the
entire frontend that knows he's different is the avatar, which keeps his rust
ring β€” and rust was deliberately held back from the palette of hashed tints
that everyone else's initials get, so no member can accidentally look like
staff. If you deleted his row, he'd simply be gone, and nothing would break.
That property is the whole point.

Direct messages live on the channel row, and that was the interesting call.
There is no channel_members join table. Public channels and DMs are the same
channels table; a DM stores its two participants in user_a_id /
user_b_id, always lower id first, with a partial unique index on the pair.
Two kinds of membership are not the same problem β€” a public channel has an
open-ended set of people and a DM has exactly two, forever β€” and modelling them
identically costs you a join on every message load plus a permanent class of
duplicate-conversation bugs. With the ordering constraint, "Maya and Dev
already have a thread" is enforced by the database, not by code that has to
remember to check. A member's entire channel list, public rooms and private
threads together, is one query.

One WebSocket, both directions. The client sends a message over the same
connection that delivers it. No POST-then-wait-for-the-echo, which means no
optimistic update to reconcile and no reordering to get wrong. Fan-out is a
dictionary of live connections: a public message goes to everyone connected, a
DM to two user ids. Barkley's reply travels the identical path β€” it is just
another message row, delivered the same way, which is why he needs no client
support at all.

Barkley's trigger check is where the character actually lives. Every
message that arrives runs through one function that decides whether he has
anything to say. A mention of @barkley. A DM he is a participant of. A
dangerous word matched on a word boundary β€” so "chocolate" fires and
"chocolatier" doesn't. Anything else returns early and he stays quiet. A guard
on his own user id means he can never answer himself, which is the failure mode
that turns this kind of feature into an infinite loop at 3am. The mention and
danger branches compose rather than firing twice: tag him about a chocolate
brownie and you get one message that answers you and warns you.

His context is deliberately not a vector store. It's the last ten messages in
the channel, plus a SELECT over every dog in the community β€” name, breed,
owner, and the description written when the photo was first seen β€” pasted into
the prompt. The model already knows what a beagle is. There is nothing to
retrieve. What it doesn't know is that Maple belongs to Priya and is a corgi,
so that's what I give it, and that's why his welcomes and his answers name real
dogs in the room.

He dispatches as a detached task after the message has already been fanned out,
so nobody waits on him. While he thinks, a typing frame goes to the same
recipients β€” and the off frame on failure is the detail I'm proudest of.
If the model returns nothing, the indicator is explicitly cleared. Without
that, one dead provider leaves "Barkley is thinking…" on screen forever, and
the app looks broken rather than quiet. I verified it by booting a second
backend pointed at a model id that doesn't exist: typing on, typing off 0.6
seconds later, socket alive, no message row, no user-visible damage.

The model layer never raises. Timeout, 404, malformed JSON, no provider at all
β€” every failure returns empty. A dead model means a dog that arrives without a
breed and a Barkley who is having a quiet day. It never means a failed
registration or a broken socket. Silence is a first-class return value here,
which felt right for a character whose defining trait is knowing when not to
speak.

Prize Categories

Best Use of Google AI.

The entire application runs on Gemini via Google AI Studio β€”
gemini-3.5-flash-lite, called directly over the REST API. One model, and I
want to be specific about the two structurally different jobs it does, because
they have opposite performance profiles and I designed around that difference.

Identity β€” seeing a dog once. When you add your dog, its photo goes to
Gemini with a prompt that asks for JSON: the most likely breed, and two or
three warm sentences about how this particular dog looks and comes across. It
comes back in about 2.3 seconds. That number is what let me run it
synchronously inside the request β€” no background job, no socket frame, no
"breed pending" state in the rail. You submit the form, and by the time the
modal closes your dog has a breed nobody typed. This result is identity: it
doesn't change, so it's written to the dog row and read back as text forever
after. It's also what gives Barkley his memory of the whole pack, at zero
further cost.

Looking β€” seeing what's happening now. This is a separate job and it runs
live. On any turn where there's a photo in Barkley's context window, he reads
that image and sends it with his prompt. Not a cached description β€” the actual
bytes, every time. It has to work that way: a question about that picture,
right now, has nothing cacheable about it. Someone posts their dog looking
guilty next to a destroyed cushion and asks what breed does this; someone posts
a bowl of grapes with no caption at all. Measured, that's 1.8 seconds with an
image against 1.0 without β€” Gemini's multimodal latency is low enough that
looking at a photo is something a conversation can afford to do routinely
rather than as a special mode.

The cached identity pass is what makes the live looking affordable, not what
replaces it. He knows every dog in the community from text, so the image
budget is spent entirely on what's in front of him.

Every line Barkley speaks is generated β€” the welcomes, the breed answers, the
DMs, and the danger warnings. The safety facts are pinned in the prompt so
they can't drift; the voice around them is Gemini's, fresh each time. That's
why he can warn you about theobromine twice in one week and not say it the
same way once.


Barkley is a good boy. He would like you to know that chocolate is not for
dogs, and he would like to tell you again in a minute.

Top comments (0)