diff --git a/variants/geprc_linkflow_900/platformio.ini b/variants/geprc_linkflow_900/platformio.ini
new file mode 100644
index 000000000..400befa4e
--- /dev/null
+++ b/variants/geprc_linkflow_900/platformio.ini
@@ -0,0 +1,136 @@
+; GEPRC LINKFLOW 900M TX - an ExpressLRS 900 MHz transmitter module.
+;
+; ESP32-D0WDQ6, 4 MB flash, SX1276 driving an external PA whose gain is set
+; from a DAC pin. The board itself is a plain ExpressLRS TX module, so the
+; two classes it uses are shared:
+;
+; helpers/esp32/ELRSTxBoard.h fan + backpack + no battery
+; helpers/radiolib/DacPaSX1276Wrapper.h DAC-controlled amplifier gain
+;
+; Every pin below is the value from the ExpressLRS hardware layout for this
+; module - ExpressLRS target "GEPRC LINKFLOW 900M TX", firmware
+; Unified_ESP32_900_TX - read back out of this module's own flash and
+; transcribed here. Two entries in that layout are deliberately unused:
+; serial_rx/serial_tx (both GPIO13, the inverted half-duplex CRSF link to the
+; handset) and the backpack UART on GPIO16/17.
+
+[GEPRC_Linkflow_900]
+extends = esp32_base
+board = esp32dev
+board_build.partitions = variants/dual_ota_1536k.csv
+build_flags =
+ ${esp32_base.build_flags}
+ -I variants/geprc_linkflow_900
+ -D GEPRC_LINKFLOW_900
+ -D MANUFACTURER_NAME='"GEPRC LINKFLOW 900M TX"'
+
+ ; ---- radio ----
+ -D RADIO_CLASS=CustomSX1276
+ -D WRAPPER_CLASS=DacPaSX1276Wrapper
+ -D P_LORA_SCLK=18
+ -D P_LORA_MISO=19
+ -D P_LORA_MOSI=23
+ -D P_LORA_NSS=5
+ -D P_LORA_RESET=14
+ -D P_LORA_DIO_0=4
+ -D P_LORA_DIO_1=21
+ -D SX127X_CURRENT_LIMIT=120
+
+ ; ---- RF switch ----
+ ; The board only brings out RX_EN (hardware.json "power_rxen"); there is no
+ ; TX_EN pin. Leaving SX127X_TXEN undefined makes RadioLib drive this pin
+ ; HIGH for RX and LOW for TX, which is what a single-pin switch wants.
+ ; NOTE: GPIO12 is also the MTDI flash-voltage strapping pin, so it has to
+ ; stay low through reset. Do not add a pull-up here.
+ -D SX127X_RXEN=12
+
+ ; ---- frequency ----
+ ; This module's own ExpressLRS options blob had "domain": 1, which is
+ ; FCC915 in FHSS.cpp (903.5 - 926.9 MHz). So it is a US 915 MHz unit, and
+ ; the 869.618 inherited from arduino_base is wrong for it. build_unflags
+ ; below drops the inherited one so this is the only definition.
+ -D LORA_FREQ=910.525
+
+ ; ---- TX power ----
+ ; ExpressLRS layout "power_apc2". The dBm-to-DAC table lives in target.cpp.
+ -D PIN_PA_APC2=26
+
+ ; These numbers are dBm at the antenna, i.e. after the amplifier. The
+ ; SX1276 itself sits at a fixed +2 dBm; see DacPaSX1276Wrapper.h.
+ ; 17 dBm (50 mW) is the stock ExpressLRS "power_default" for this board.
+ ;
+ ; LORA_TX_POWER must stay within 2..17. CustomSX1276::std_init() passes it
+ ; straight to RadioLib's begin(), and PA_BOOST rejects anything outside that
+ ; range, which would fail radio_init(). It only sets the startup level;
+ ; MIN/MAX below are what bound the range at runtime, and those go through
+ ; the DAC, so they are free to be higher.
+ ;
+ ; MAX_LORA_TX_POWER is defined once and used twice: the CLI validates
+ ; against it, and target.cpp hands the same value to the wrapper, which
+ ; enforces it in applyCachedTxPower(). The wrapper also refuses anything
+ ; outside its own table regardless, so a board that omits these flags is
+ ; still bounded by what the amplifier table actually describes.
+ ;
+ ; The amplifier will do 33 dBm (2 W), but ExpressLRS only ever asks for that
+ ; in short packet bursts. MeshCore transmits for far longer, so the ceiling
+ ; here is one table entry lower. Raise it to 33 only with cooling you
+ ; trust, and check your local duty-cycle and EIRP limits first.
+ -D LORA_TX_POWER=17
+ -D MIN_LORA_TX_POWER=10
+ -D MAX_LORA_TX_POWER=30
+
+ ; ---- board ----
+ -D PIN_FAN_EN=32 ; hardware.json "misc_fan_en"
+ -D PIN_BACKPACK_EN=25 ; hardware.json "backpack_en"
+ -D PIN_BACKPACK_BOOT=15 ; hardware.json "backpack_boot"
+
+ ; WS2812 on GPIO27. hardware.json says GRB ordering, which is what
+ ; neopixelWrite() emits.
+ -D P_LORA_TX_NEOPIXEL_LED=27
+
+; The CH340 on this board is not reliable above 230400 for flash access;
+; 460800 and 921600 both corrupt mid-transfer.
+upload_speed = 115200
+build_unflags =
+ -D LORA_FREQ=869.618
+ ; esp32_base turns LoRa OTA (mOTA) on for every ESP32 target. Its context
+ ; is ~15.6 KiB of .bss - two 4 KiB Merkle proof scratch buffers and two
+ ; 2 KiB block buffers - resident whether or not the node ever serves an
+ ; image. Classic ESP32 cannot afford it. Dropping ENABLE_OTA compiles the
+ ; whole engine out (MyMesh.h only includes OtaContext.h when it is set).
+ -D ENABLE_OTA=1
+ -D OTA_FLASH_STORE=1
+ -D OTA_FOLDER_SERIAL
+build_src_filter = ${esp32_base.build_src_filter}
+ +<../variants/geprc_linkflow_900>
+ -
+lib_deps =
+ ${esp32_base.lib_deps}
+
+[env:GEPRC_Linkflow_900_repeater]
+extends = GEPRC_Linkflow_900
+build_flags =
+ ${GEPRC_Linkflow_900.build_flags}
+ -D ADVERT_NAME='"Linkflow Repeater"'
+ -D ADVERT_LAT=0.0
+ -D ADVERT_LON=0.0
+ -D ADMIN_PASSWORD='"password"'
+ -D MAX_NEIGHBOURS=50
+ ; --- classic ESP32 DRAM strip ---------------------------------------
+ ; These are all fixed .bss tables, sized for a comfortable target and
+ ; allocated whether or not a single rule is ever configured. Stock cost on
+ ; this build was ~18.5 KiB of flood tables plus 9.6 KiB of client ACL.
+ ; The node still repeats; it just has almost no room for forwarding rules,
+ ; scope rewrites, group moderation, or logged-in clients.
+ -D MAX_CLIENTS=2 ; was 32, at ~300 B/entry
+ -D MESH_ENABLE_FLOOD_RULE_ENGINE=0 ; drops the forward-rule engine
+ -D FLOOD_PACKET_FILTER_SLOTS=1 ; was 63, at 200 B/entry
+ -D FLOOD_CHANNEL_SCOPE_SLOTS=1 ; ESP32 default is 255; also drives
+ ; DIRECT_SCOPE and SCOPE_REQUIRE
+ -D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; drops text moderation
+ -D FLOOD_GROUP_MODERATION_SLOTS=1 ; was 16, at 124 B/entry
+build_src_filter = ${GEPRC_Linkflow_900.build_src_filter}
+ +<../examples/simple_repeater>
+lib_deps =
+ ${GEPRC_Linkflow_900.lib_deps}
+ ${esp32_ota.lib_deps}
diff --git a/variants/geprc_linkflow_900/target.cpp b/variants/geprc_linkflow_900/target.cpp
new file mode 100644
index 000000000..3522b167f
--- /dev/null
+++ b/variants/geprc_linkflow_900/target.cpp
@@ -0,0 +1,51 @@
+#include
+#include "target.h"
+
+ELRSTxBoard board;
+
+static SPIClass spi;
+RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
+
+// The "power_values" array from the ExpressLRS hardware layout for this
+// module (ExpressLRS target "GEPRC LINKFLOW 900M TX", firmware
+// Unified_ESP32_900_TX), paired with the PowerLevels_e step each index maps
+// to. power_min is 0 on this board, so index 0 is PWR_10mW.
+static const DacPaLevel POWER_LEVELS[] = {
+ { 10, 30 }, // 10 mW
+ { 14, 40 }, // 25 mW
+ { 17, 50 }, // 50 mW <- ExpressLRS "power_default"
+ { 20, 60 }, // 100 mW
+ { 24, 80 }, // 250 mW
+ { 27, 90 }, // 500 mW
+ { 30, 130 }, // 1000 mW
+ { 33, 225 }, // 2000 mW <- above MAX_LORA_TX_POWER, see platformio.ini
+};
+
+// MAX_LORA_TX_POWER is the single definition of this board's ceiling: the CLI
+// validates against it, and the wrapper enforces it independently so a request
+// that gets past the CLI cannot saturate the amplifier at its top table entry.
+WRAPPER_CLASS radio_driver(radio, board, PIN_PA_APC2,
+ POWER_LEVELS,
+ sizeof(POWER_LEVELS) / sizeof(POWER_LEVELS[0]),
+ MAX_LORA_TX_POWER);
+
+ESP32RTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+SensorManager sensors;
+
+bool radio_init() {
+ fallback_clock.begin();
+ rtc_clock.begin(Wire);
+
+ if (!radio.std_init(&spi)) return false;
+
+ // std_init() left the SX1276 at LORA_TX_POWER, which on this board is a
+ // post-amplifier number. Hand power control over to the amplifier instead.
+ radio_driver.beginPowerControl(LORA_TX_POWER);
+ return true;
+}
+
+mesh::LocalIdentity radio_new_identity() {
+ RadioNoiseListener rng(radio);
+ return mesh::LocalIdentity(&rng); // create new random identity
+}
diff --git a/variants/geprc_linkflow_900/target.h b/variants/geprc_linkflow_900/target.h
new file mode 100644
index 000000000..6a6022163
--- /dev/null
+++ b/variants/geprc_linkflow_900/target.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern ELRSTxBoard board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern SensorManager sensors;
+
+bool radio_init();
+mesh::LocalIdentity radio_new_identity();