RECEIPT · 2026-09-06 · the FrankenKeys log

One file, whole keyboard

How a complete FrankenKeys build, theme, effects, sounds and background photo, travels to another phone as a code, a QR or one file, with no server.

2026-09-06 · by Corey Redwine · share a keyboard theme as a file

A keyboard with no server has to solve sharing without one. FrankenKeys does it in three layers, each carrying more than the last, and every layer is a format you can read with a text editor or an unzip tool. This is what each layer holds and why the third one had to be a file.

Layer one: a theme is a line of text

A custom theme is seven colours, a glow mode, a photo-dim value and a font name. That fits in a token:

FB1:BG.KEY.TEXT.ACCENT.GLOW.TRAIL.RIPPLE;g=1;d=40;f=orbitron

Seven hex colours, then ;key=value pairs. Two optional pairs ride along: x= for an Effect Forge design and s= for a Shader Forge tune. The token contains no whitespace, no semicolons inside values and no equals signs, so it survives being pasted into the middle of a sentence and is found again on the other end by looking for FB1:.

Codes written before the rename began APEX1:. They still decode, and always will. A code somebody saved in a note is not mine to break.

Layer two: a scene is a theme plus everything around it

A scene is the whole look of the board: the theme, both press effects, the trail, the wave, particles, the shader and its tune, ambient mode, key shape, press animation, the sound profile, the haptic profile, the font, every per-key override and the macro row. That is also a line of text:

FB2:[FB1:...;g=...;d=...;f=...;]k=v;k=v;...

When the scene's theme is custom, the body starts with the theme's FB1: token, so a build that predates scenes finds the FB1: inside and imports the theme, ignoring the keys it does not know. Every key is optional on decode: a missing key takes the default, an unknown key is skipped, a bad value is clamped. Garbage never throws. Values that could contain anything, like the JSON of a key override, are percent-escaped so the grammar holds.

Free keeps two saved scenes; Pro keeps as many as you like. Sharing them costs nothing either way.

Layer three: the picture is the problem

The one thing people most want to send is their background photo, and it cannot ride in a code. A QR tops out around two kilobytes and a photo is two hundred times that. Hiding the payload inside the shared image does not work either, because every messenger recompresses pictures and strips metadata, so the data would die on exactly the paths people share through.

So a picture and a payload are two artefacts, and the payload is an ordinary zip with the extension .forgeboard:

setup.txt          first line: FORGEBOARD SETUP 1, then the FB2 scene code
background.jpg     the photo, if there is one

The header says FORGEBOARD, not FrankenKeys, because it is a format tag and bundles already shared between phones have to keep opening after the rename. The MIME type is deliberately application/octet-stream rather than zip, because messengers hide "archives" behind warnings and this is a document as far as the share sheet needs to know.

N/I

Unzip one. Share a scene from Settings, Scenes, as a file, then open the .forgeboard with any archive tool. Two entries, one of them plain text starting FORGEBOARD SETUP 1. Nothing else is in there, and nothing in it identifies you or the phone.

Reading is the dangerous direction

A .forgeboard file arrives from a stranger, so the importer trusts nothing. It accepts exactly the two entry names it knows, which is also what makes path traversal impossible. It counts entries, and it caps both per-entry and total uncompressed size so a zip bomb runs out of budget instead of memory. The picture comes back as raw bytes and is decoded bounds-first and re-encoded by the caller; the bytes in the file are never written to disk as they arrived. The whole reader is plain Java io and zip with no Android in it, so each of those limits is a unit test rather than a hope.

On the receiving phone, an imported scene shows a live preview before you keep it. The description on Play says "there is no decline button", which is a joke; there is, it is called not tapping Keep.

What never travels

Two things are excluded on purpose. Imported fonts are not in any code or bundle, because a font file has a licence and I am not going to redistribute yours. And the Pro entitlement is stripped from every export, backup included, because an exported flag is a transferable licence and a backup file must never become one.

The share sheet is the gallery

There is no theme gallery, and the no-server post says why. What there is instead: three formats, one of them a QR you can put on a poster, one of them a line you can paste into any chat, and one of them a file that survives anything that carries files. The drop scenes, the keyboard half of a FrankenDrop drop, use the same formats, which is what lets a case and a keyboard theme be one design in two materials.

Where the formats are defined

  • studio.forgedev.forgeboard.theme.ThemeShare: the FB1: grammar and the APEX1: legacy decode.
  • studio.forgedev.forgeboard.core.SceneCodec: the FB2: grammar and every key it accepts.
  • studio.forgedev.forgeboard.core.SetupBundle: the zip layout, the header, and the size caps.
  • All three are readable in the shipped build: how to open it.

Formats outlive names. That is why the header still says ForgeBoard, and why a code from the ApexBoard days still opens today.