A metal tag engraved with a phone number has three failure modes: the number changes, the engraving wears off, and the finder has to call a stranger.
Better model: make the pet's collar a URL.
The tag is a pointer, not the data
Don't store data on the tag. An NTAG215 chip holds ~504 bytes — enough for a URL, not a profile. And you could never update it.
Instead:
NFC tag -> https://example.com/p/{pet_id}
QR code -> https://example.com/p/{pet_id}
The tag is a pointer. The profile lives on the server. That gives you mutable data (owner changes their number, no new collar), scan analytics, field-level access control, and revocability.
The physical tag becomes dumb infrastructure. That's the point. This is the model we use for ProfileTap's pet ID tags.
Why NFC and QR
They're not redundant — they cover different failure modes.
NFC wins when:
- It's dark — no camera, no aiming
- The surface is scratched or dirty
- You want a one-second, zero-thought interaction
QR wins when:
- The finder's phone has no NFC chip
- The finder doesn't know what NFC is, or where to tap
- The tag is behind glass, or out of tapping reach
In India, a large chunk of phones don't have NFC. Ship NFC-only and half your finders can't use it. Print both on the same tag. The QR is your fallback — and fallbacks aren't optional when the failure mode is "the pet doesn't come home."
The same dual NFC+QR approach applies well beyond pets — we use it for vehicle windshield tags and luggage tags too.
Writing the tag
NDEF is the standard. One record, type URI:
const ndef = new NDEFReader();
await ndef.write({
records: [{ recordType: "url", data: "https://example.com/p/abc123" }]
});
Lock the tag after writing. NTAG215 has a one-way lock bit. An unlocked tag can be overwritten by anyone with a phone — including someone who doesn't want the pet returned.
Design for a stranger, not the owner
The person scanning is a stranger who found a scared animal on the road. They have ~30 seconds of patience.
- No login. No app install. Web only.
- One primary action above the fold — a big "Call Owner" button.
- Server-render it. They may be on weak 3G on a roadside.
- No home address. Anyone who scans a stray tag would then know an address and that the resident owns a pet. Keep it server-side.
Location: ask, don't take
Tell the owner where the pet was found:
navigator.geolocation.getCurrentPosition(
(pos) => notifyOwner(pos.coords.latitude, pos.coords.longitude),
() => {} // declined - degrade gracefully
);
But never gate the phone number behind the permission prompt. A system that holds the pet's rescue hostage to a browser dialog is a system that fails.
Privacy is the whole product
A scannable public URL is, by definition, a public URL.
- Non-sequential IDs — random base62, not auto-increment. Enumeration should be pointless.
- Rate-limit by IP — someone hitting 500 profiles isn't looking for a lost dog.
- Call masking — route calls through a proxy number so the owner's real number is never exposed. We wrote about this pattern in more depth here.
- Log every scan so the owner can see the history.
Wrapping up
The stack is boring — a URL, an NDEF record, a server-rendered page, a notification queue. That's a feature. The hard parts aren't technical; they're the 30 seconds of attention you get from a stranger on the roadside.
We're building this at ProfileTap — NFC + QR smart profiles for pets, vehicles, families and businesses, built India-first.
How did you handle the location-consent tradeoff? Drop a comment.
Top comments (0)