A hybrid between git, Syncthing, and a Headless CMS.
You interact with a synced database representation of your files, enabling offline editing and lazy syncing — all through your mobile device or browser.
┌──────────────────────────────────────────────────────────────────┐
│ Your PC / Laptop │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ File System │───▶│ filePort Agent (Node.js + Chokidar) │ │
│ └──────────────┘ │ • Scans watched folders on startup │ │
│ │ • Pushes changes in real time │ │
│ │ • Reconciler pulls mobile edits back │ │
│ └──────────────────┬───────────────────┘ │
└─────────────────────────────────────────┼────────────────────────┘
│ HTTP / REST
┌───────────▼──────────────┐
│ PocketBase (SQLite) │
│ files collection │
│ • path, name, content │
│ • last_modified_local │
│ • last_modified_mobile │
│ • needs_sync flag │
└───────────┬──────────────┘
│ HTTP / REST
┌────────────────▼──────────────────┐
│ filePort Web UI (Xterm.js) │
│ • ls / cd / cat / edit commands │
│ • In-browser text editor │
│ • Save → sets needs_sync = true │
└───────────────────────────────────┘
(open on phone via Tailscale)
| Direction | Trigger | Mechanism |
|---|---|---|
| PC → DB | File created/changed/deleted | Chokidar event → upsert record |
| DB → PC | needs_sync = true in DB |
Reconciler loop (every 10 s) |
| Mobile → DB | User saves in Web UI | POST /api/save sets content + flag |
| Layer | Technology |
|---|---|
| Agent / file watcher | Node.js + Chokidar |
| Database / sync layer | PocketBase (single-file SQLite backend) |
| Mobile / web UI | Xterm.js + Express.js |
| React Native mobile | React Native + @supabase/supabase-js |
| Remote connectivity | Tailscale (zero-config VPN) |
filePort/
├── agent/ # Node.js desktop agent
│ ├── src/
│ │ ├── index.js # Entry point (CLI flags: --reconcile-only, --index-only)
│ │ ├── indexer.js # Scans watched folders; builds + upserts file records
│ │ ├── watcher.js # Chokidar real-time watcher
│ │ ├── reconciler.js # Applies mobile edits back to the local filesystem
│ │ └── pocketbase.js # PocketBase REST API client
│ ├── __tests__/ # Jest unit tests
│ │ ├── indexer.test.js
│ │ └── reconciler.test.js
│ ├── config.example.json # Copy to config.json and edit
│ └── package.json
│
├── mobile/ # React Native CLI component (Supabase)
│ ├── src/
│ │ ├── components/
│ │ │ ├── TerminalCLI.jsx # Terminal UI: ls, cd, pwd, edit, help
│ │ │ └── FileEditor.jsx # Full-screen dark-mode text editor
│ │ ├── hooks/
│ │ │ └── useRemoteFiles.js # Supabase query hook
│ │ ├── lib/
│ │ │ └── supabase.js # Supabase client factory
│ │ └── utils/
│ │ └── pathUtils.js # Path resolution utilities
│ ├── __tests__/ # Jest + @testing-library/react-native tests
│ │ ├── pathUtils.test.js
│ │ ├── useRemoteFiles.test.jsx
│ │ ├── TerminalCLI.test.jsx
│ │ └── FileEditor.test.jsx
│ ├── .env.example # SUPABASE_URL + SUPABASE_ANON_KEY
│ ├── babel.config.js
│ └── package.json
│
├── sync/ # Python Supabase pending_sync poller
│ ├── supabase_sync.py # Polling script (runs every 60 s by default)
│ ├── .env.example # SUPABASE_URL + SUPABASE_SERVICE_KEY
│ ├── requirements.txt # supabase, python-dotenv
│ └── tests/
│ └── test_supabase_sync.py # pytest unit tests (fully mocked)
│
├── ui/ # Web terminal UI
│ ├── server.js # Express server (proxies PocketBase calls)
│ ├── public/
│ │ └── index.html # Xterm.js terminal SPA
│ └── package.json
│
├── pocketbase/
│ └── pb_schema.json # PocketBase collection schema
│
└── README.md
- Node.js ≥ 18
- PocketBase binary — download and place in
pocketbase/ - (Optional) Tailscale for remote access
cd pocketbase
./pocketbase serveOpen http://127.0.0.1:8090/_/ and create an admin account.
Import the schema:
# Use PocketBase's import endpoint or paste pb_schema.json into the admin UI
./pocketbase import pb_schema.jsoncd agent
cp config.example.json config.json
# Edit config.json:
# • watchedFolders – absolute paths you want to sync
# • pocketbaseUrl – where PocketBase is running
# • pocketbaseEmail / pocketbasePassword – admin credentials{
"watchedFolders": ["/home/alice/Documents", "/home/alice/Notes"],
"pocketbaseUrl": "http://127.0.0.1:8090",
"pocketbaseEmail": "admin@example.com",
"pocketbasePassword": "supersecret",
"textExtensions": [".txt", ".md", ".py", ".js", "..."],
"reconcileIntervalMs": 10000,
"maxFileSizeBytes": 1048576
}Install dependencies and start the agent:
npm install
npm startOn first run the agent:
- Authenticates with PocketBase.
- Performs a full index scan of all watched folders.
- Starts the real-time file watcher (Chokidar).
- Runs a reconcile loop every
reconcileIntervalMsmilliseconds.
cd ui
npm install
POCKETBASE_URL=http://127.0.0.1:8090 \
POCKETBASE_EMAIL=admin@example.com \
POCKETBASE_PASSWORD=supersecret \
npm startOpen http://localhost:3000 in your browser (or via Tailscale on your phone).
Install Tailscale on both your PC and phone, then log in:
# PC
sudo tailscale up
# Point your phone browser at your PC's Tailscale IP:
# http://100.x.x.x:3000The Web UI will feel like an SSH session to your file system.
The mobile/ package provides a self-contained TerminalCLI React Native component that connects directly to Supabase (instead of PocketBase) to power an alternative mobile experience.
Create a Supabase project at supabase.com and run the following SQL to create the remote_files table:
CREATE TABLE remote_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
path TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
content TEXT,
is_directory BOOLEAN DEFAULT FALSE,
last_modified_local TIMESTAMPTZ,
last_modified_mobile TIMESTAMPTZ,
needs_sync BOOLEAN DEFAULT FALSE, -- agent → mobile: local file changed
pending_sync BOOLEAN DEFAULT FALSE, -- mobile → agent: mobile edit ready to push
file_size INTEGER DEFAULT 0,
mime_type TEXT
);
CREATE INDEX ON remote_files (path);
CREATE INDEX ON remote_files (needs_sync);
CREATE INDEX ON remote_files (pending_sync);Sync flags:
needs_syncis set by the desktop agent when a local file changes (signalling to mobile that fresh content is available).pending_syncis set by the mobile app when the user saves an edit (signalling to the agent that mobile changes are ready to pull).
cd mobile
cp .env.example .env # fill in SUPABASE_URL and SUPABASE_ANON_KEY
npm installimport { createSupabaseClient } from './src/lib/supabase';
import { TerminalCLI } from './src/components/TerminalCLI';
const supabase = createSupabaseClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
export default function App() {
return <TerminalCLI supabase={supabase} initialPath="/" />;
}| Command | Description |
|---|---|
ls [path] |
List files/directories (from remote_files table) |
cd <path> |
Navigate the virtual directory (supports .., absolute paths) |
edit <file> |
Open a file in the full-screen editor; Save sets pending_sync = true |
pwd |
Print current directory |
help |
Show available commands |
The sync/ package provides a lightweight Python script that runs on the desktop and polls the Supabase remote_files table every 60 seconds. Whenever it finds a row with pending_sync = true (written there by the mobile app after a file save), it:
- Reads the
contentcolumn from the database. - Overwrites the local file at
pathwith that content (creating parent directories if needed). - Clears
pending_syncand stampslast_modified_localin the database.
This completes the mobile-→-desktop sync direction without requiring the Node.js agent.
cd sync
cp .env.example .env
# Edit .env and fill in SUPABASE_URL and SUPABASE_SERVICE_KEY
# (use the service-role key so the script can bypass RLS)
pip install -r requirements.txt
python supabase_sync.py| Variable | Description |
|---|---|
SUPABASE_URL |
Your Supabase project URL (https://<ref>.supabase.co) |
SUPABASE_SERVICE_KEY |
Service-role key (never expose this publicly) |
POLL_INTERVAL_SECONDS |
Polling cadence in seconds (default: 60) |
cd sync
pip install -r requirements.txt pytest
python -m pytest tests/ -v| Command | Description |
|---|---|
ls [path] |
List files and directories at the given path (defaults to cwd) |
cd <path> |
Change virtual directory (supports ..) |
cat <file> |
Print file content |
edit <file> |
Open the in-browser editor; click Save to sync back to PC |
pwd |
Print current directory |
clear |
Clear the terminal |
help |
Show available commands |
Every reconcileIntervalMs milliseconds the agent:
- Queries PocketBase for records where
needs_sync = true. - For each record, compares
last_modified_mobilevslast_modified_local. - If mobile is newer, the agent overwrites the local file with DB content and clears the
needs_syncflag. - If local is already up-to-date, it just clears the flag.
# Agent (Node.js)
cd agent
npm test
# Mobile React Native component
cd mobile
npm test
# Python Supabase sync poller
cd sync
pip install -r requirements.txt pytest
python -m pytest tests/ -vAgent tests cover the indexer (file scanning, record building) and reconciler (conflict resolution, file writing).
Mobile tests cover pathUtils (pure path logic), useRemoteFiles (Supabase hook with mocked client), TerminalCLI (component rendering and command handling via @testing-library/react-native), and FileEditor (full-screen editor component).
Python tests cover fetch_pending, write_file, clear_pending, process_row, and run_sync_cycle — all with a fully mocked Supabase client and a temporary directory for filesystem operations.
| Flag | Behaviour |
|---|---|
| (none) | Full mode: index scan + watcher + reconcile loop |
--index-only |
Run one full index scan then exit |
--reconcile-only |
Run one reconcile pass then exit |
| Field | Type | Description |
|---|---|---|
path |
text (unique) | Absolute path on the PC |
name |
text | Filename or directory name |
content |
text | File content (text files only; empty for binaries/dirs) |
is_directory |
bool | true for directories |
last_modified_local |
date | Last time the agent wrote this record |
last_modified_mobile |
date | Set when the user saves from the Web UI |
needs_sync |
bool | true → reconciler should apply mobile edits to PC |
file_size |
number | Size in bytes (0 for directories) |
mime_type |
text | MIME type (e.g. text/plain, image/png) |
- Never expose PocketBase directly to the internet; keep it behind Tailscale.
- The Web UI server proxies all PocketBase requests — credentials stay server-side.
- Use strong admin credentials and rotate them regularly.
- The
listRule/viewRule/ etc. on thefilescollection require authentication — anonymous access is disabled.
- Binary file upload (PocketBase file storage)
- Conflict detection (both sides changed simultaneously)
- Push notifications (PocketBase real-time subscriptions)
- React Native mobile app
- Multi-user / multi-device support