Transmate
Inspiration
I often needed to move a link, command, code snippet, or short piece of text between my computer and phone. Messaging the content to myself worked, but it mixed temporary information with normal conversations and required both devices to use the same messaging account.
I wanted something simpler: open a webpage, enter a shared passphrase, paste the content, and retrieve it from another device.
That idea became Transmate, a lightweight cross-device clipboard that does not require users to create an account.
What it does
Transmate organizes pasted text and links into private spaces protected by passphrases. A user can create a space on one device and enter the same passphrase on another device to access its contents.
The application supports:
- Passphrase-protected spaces without a traditional account system
- Cross-device transfer of text, links, commands, and code snippets
- Optional notes that explain what each saved item is for
- Search across both saved content and notes
- Filtering by date, device, and content type
- Custom date ranges for locating older records
- Server-side pagination so large spaces do not load everything at once
- One-click copying and deletion
- Device-based limits on creating new spaces
- A first-visit onboarding guide
- An administrative dashboard for managing spaces and backups
- A separate link-processing tool that loads only when requested
Each space is isolated on the server. Requests can only access the space associated with the user’s signed session, preventing search results or records from leaking across spaces.
How I built it
I built Transmate as a lightweight web application using:
- Node.js 24
- Node.js’s built-in
node:sqliteAPI - Vanilla JavaScript
- HTML and CSS
- SQLite
- Nginx
- Debian
- Let’s Encrypt HTTPS
- Cloudflare DNS
The frontend uses standard browser APIs, including the Clipboard API and localStorage for generating a local device identifier.
Instead of usernames and passwords, Transmate stores each passphrase using PBKDF2 with a unique random salt. After successful verification, the server creates a signed, HTTP-only session cookie containing the space identifier and expiration time.
The original version stored data in a JSON file. This was convenient during early development, but every update required rewriting the complete file. As the project grew, I migrated the data layer to SQLite.
The SQLite version uses separate tables for spaces, saved items, device creation records, and device-to-space relationships. Indexes on space identifiers and creation timestamps allow the server to retrieve only the requested page of records.
The migration system imports the existing JSON data inside a transaction, then verifies every space, item, note, passphrase hash, and device record before enabling the new database. The original JSON file remains available as a rollback snapshot.
The application is deployed behind Nginx on a Debian cloud server. Nginx handles HTTPS, redirects, static resources, and reverse proxying to the Node.js service.
Challenges
Protecting spaces without accounts
Removing accounts made the interface simpler, but it meant the passphrase had to act as both the identifier and access credential. I needed to verify passphrases securely while ensuring that plaintext passphrases were never stored.
I solved this with salted PBKDF2 hashes, timing-safe comparisons, signed sessions, login rate limiting, and strict server-side space isolation.
Preventing the page from growing indefinitely
The first implementation loaded every record in a space and filtered it in the browser. This worked with a small dataset, but it would eventually create long pages and slow loading times.
I redesigned the API to support server-side pagination, search, device filters, content-type filters, and precise date ranges. The browser now requests only 20 records at a time.
Migrating live data safely
Moving from JSON to SQLite was one of the most sensitive parts of the project because users had already saved real content.
Before switching databases, I created timestamped backups and a migration verifier. The verifier compared every migrated record with its JSON source. The production migration successfully preserved all existing spaces, items, notes, devices, and passphrase hashes without modifying the original JSON file.
Deploying HTTPS and changing servers
The project was migrated between Debian cloud servers while preserving data and minimizing downtime. I used release directories and a current symbolic link so that a failed deployment could immediately return to the previous version.
I also encountered unreliable DNS resolution while requesting one HTTPS certificate. Testing with another domain helped isolate the problem to the DNS provider rather than Nginx or the application.
What I learned
This project taught me that a small application can still involve meaningful security, data integrity, and operational decisions.
I learned how to:
- Design authentication without a traditional account system
- Build server-side search, filtering, and pagination
- Model application data in SQLite
- Perform a verified migration from JSON to SQLite
- Protect user spaces at the API level
- Configure Nginx and HTTPS on Debian
- Deploy releases with rollback support
- Preserve production data while iterating on a live service
Most importantly, I learned to treat migration and recovery as part of the feature itself. A successful deployment is not only one that starts correctly, but one that can prove existing data was preserved and can safely return to the previous version if something goes wrong.
What’s next
Future improvements could include optional expiration times, automatic cleanup policies, encrypted content storage, improved abuse prevention, QR-code access between devices, and full-text search using SQLite FTS5.
Log in or sign up for Devpost to join the conversation.