This season is one of gift-giving.
Somehow, I found myself participating in three fairly high-effort gift exchanges at the same time: a crafting Secret Santa, a words of affirmation Secret Santa, and a creative tech Secret Santa.
As someone who’s chronically overcommitted, I decided to use the creative tech Secret Santa as a prompt for my final project in Connections Lab.
Out of all the many strangers and acquaintances in the hat, the person I was assigned was my good friend Connie.

There are two things Connie loves doing with her friends: hanging out, and locking in. This is well-documented through her many messages and Partiful invites. I wanted to capture this chase for both in a physical object: something she could carry with her that expressed her current state in a simple, legible way… like a keychain.

Exhibit A

Exhibit B

Exhibit C
I wanted to make her a physical button connected to the internet. Where pressing the button switches Connie’s status between “hanging out” and “locked in,” which is then reflected live on a website that anyone can view.
The technical challenge was straightforward in theory but nontrivial in practice. I had an ESP32 microcontroller connected to my laptop via USB. I wrote code so that when the built-in button is pressed, it outputs a 1; when pressed again, it outputs a 0.
if (reading == LOW && lastStable == HIGH) {
state = !state;
Serial.println(state ? "1" : "0");
}
But The problem is that the website lives online, and I needed to make an HTTPS post directly from the development board. This was the system we needed to build:

The ESP32 sends its button state directly to a cloud-hosted Express server on Replit via HTTPS. The server then broadcasts the updated state to every connected browser using WebSockets.
Using the WiFi library, I could translate between physical hardware and the internet. By adding my Wifi information, I can connect the board directly to the internet, and send its button state as an HTTP POST request, and sends it to the server.
I wasn’t the most familiar with implementing the Arduino → web connection, particularly best practices in detecting a connection from a port we learned in the summer. ChatGPT assisted with writing functions to cover errors, a heartbeat message, and edge cases to help me catch disconnections, from both the hardware component, and the browser.
void sendState(bool state) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
http.begin(SERVER_URL);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-API-Key", API_KEY);
String body = "{\\"state\\": " + String(state ? 1 : 0) + "}";
http.POST(body);
http.end();
}
Once the server receives a state update, it needs to propagate that change instantly. I used WebSockets to maintain persistent connections between the server and all connected clients. A single button press triggers the server to broadcast the new state and timestamp to every open browser immediately.
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ server });
function broadcastState() {
const message = JSON.stringify({
state: currentState,
timestamp: getTimestamp(),
lastButtonPress: lastButtonPress,
});
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(message);
}
});
}
Using the broadcasted state, I rendered a floating text GIF corresponding to whether Connie was hanging out vs. locking in, along with a timestamp (which Replit Agent helped me write code to extract from the data):


But thinking through the user experience, I realized that no one is actually going to keep a dedicated webpage open and visible at all times (unless they are stalking her), so I started treating the browser tab itself as part of the design. Just like any other DOM element, I can also change the tab title and favicon the same way.