Secure cross-platform Python grpc daemon design
| created | 2026-08-29 15:19 |
| modified | 2026-08-29 16:15 |
| status | stub |
These are working notes for my hobby project light, which provides programmatic access for Light devices without using the web dashboard. Since this is going to handle potentially sensitive user data this can’t be vibecoded.
Currently light is an API/CLI. I have one confirmed consumer - Light Phone Manager, which is an Electron app implementing a GUI presentation layer on top of my CLI, shelling out and reading JSON responses from it. light is implemented in Python which raises a lot of issues in this scenario:
- LPM has to bundle a light executable for every platform (not too bad, but annoying)
- LPM reinvokes the executable on every single command, meaning every command has the overhead of starting the interpreter, importing everything, etc. which adds noticeable latency to all light-dependent operations within the app.
This note explores options to avoid this.
Requirements:
- must be cross-platform, preferably with as much portability and reusability as possible
- must be secure (data encrypted in transit)
- must be language-agnostic
Design notes
The chosen design is a grpc daemon (or service). The existing CLI json output flow will remain intact, and its use cases will be relegated to simple scripts (e.g. a bash script handling batch uploads).
Background
grpc is a remote-procedure call (RPC) framework developed by Google which lets a client application directly call functions on a server across a network as if they were local.
Interface and transport
grpc uses protocol buffers as its interface definition language. These are defined in .proto files, and this is the contract by which all communications happen. Code generation tools take .proto files and automatically generate stubs for any language.
Messages are sent over HTTP/2.
RPC types
| kind | signature shape |
|---|---|
| unary | rpc F(req) returns (Resp) |
| server-streaming | rpc F(req) returns (stream Msg) |
| client-streaming | rpc F(stream Msg) returns (Resp) |
| bidirectional streaming | rpc F(stream A) returns (stream B) |
How these map to current light commands:
- unary: read-only stuff like getting tracks, notes
- server-streaming: uploading tracks (returns per-file results as uploads happen)
- client-streaming: currently none, but this would be used if the client ever streamed file bytes (extremely likely in the near future)
- bidirectional: none. (a sample use case would be real-time chat applications, collaborative editing tools, etc. - not relevant)
Client/server implementation
Client
The client will need a channel and stubs. The channel is the connection to the host:port for the grpc server, and the stubs are the autogenerated RPCs from the protobuf.
channel = grpc.insecure_channel("127.0.0.1:50510")
It’s lazy (==TODO== ?), auto-reconnects if dropped, and can carry concurrent calls. insecure_channel here means no TLS, which is ok because this is on loopback. (Ensure the daemon loudly refuses non-loopback binds.)
Server
On the server end, it needs a servicer and a threadpool.
The generated code provides a base class with one empty method per RPC. I would subclass them and fill them in.
grpc.server(...)would run incoming calls on a threadpool.- Each call is one pooled thread invoking my RPC methods. I’d set threadpool to have 1 worker for forced serialization.
Security
- No TLS.
- Loopback = no network to protect.
- Bind to localhost only.
- Assign one random token per daemon start.
- Client sends it as
authorizationmetadata on every call, and a serverside interceptor checks it and rejects everything else. - Get the token to the client without a disk file.
- Client sends it as
- Don’t keep password in memory - just session token.
- A same-user process wins; nothing we can do about that.