wayver's git archive


a last value key/value store
git clone https://git.wayver.dev/afterimage

Commit: 1c2b5e408f1e8e2d2d7b4338dc096841ac46cfcd (tree) main
Author: wayverd
Date: 2026 M05 2, Sat 14:31:54 -0400
9 files changed; 1052 insertions 0 deletions
initial mvp

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..986faf3
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+    "[rust]": {
+        "editor.formatOnSave": true,
+        "editor.defaultFormatter": "rust-lang.rust-analyzer"
+    }
+}<
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..af11755
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,678 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "afterimage-client"
+version = "0.1.0"
+dependencies = [
+ "form_urlencoded",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "serde",
+ "serde_urlencoded",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "afterimage-server"
+version = "0.1.0"
+dependencies = [
+ "ahash",
+ "dashmap",
+ "form_urlencoded",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "serde",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tokio"
+version = "1.52.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
+dependencies = [
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..d1fd792
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,22 @@
+[workspace]
+resolver = "2"
+members = ["./client", "./server"]
+
+[workspace.dependencies]
+ahash = "0.8.12"
+dashmap = "6.1.0"
+form_urlencoded = "1.2.2"
+http-body-util = "0.1.3"
+hyper = { version = "1.9.0", features = ["client", "server", "http1"] }
+hyper-util = { version = "0.1.20", features = ["tokio"] }
+serde = { version = "1.0.228", features = ["derive"] }
+serde_path_to_error = "0.1.20"
+serde_urlencoded = "0.7.1"
+thiserror = "2.0.18"
+tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "fs", "net"] }
+tracing = "0.1.44"
+tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+
+[profile.release]
+lto = true
+codegen-units = 1
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4beaa3e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# afterimage
+
+a last value key/value store
diff --git a/client/Cargo.toml b/client/Cargo.toml
new file mode 100644
index 0000000..05b2cb2
--- /dev/null
+++ b/client/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "afterimage-client"
+version = "0.1.0"
+edition = "2024"
+
+workspace = ".."
+
+[dependencies]
+form_urlencoded.workspace = true
+http-body-util.workspace = true
+hyper-util.workspace = true
+hyper.workspace = true
+serde_urlencoded.workspace = true
+serde.workspace = true
+thiserror.workspace = true
+tokio.workspace = true
+tracing.workspace = true
diff --git a/client/src/lib.rs b/client/src/lib.rs
new file mode 100644
index 0000000..d85f3ce
--- /dev/null
+++ b/client/src/lib.rs
@@ -0,0 +1,173 @@
+use std::path::PathBuf;
+
+use http_body_util::{BodyExt as _, Full};
+use hyper::{Method, Request, StatusCode, Uri, body::Bytes, client::conn};
+use hyper_util::rt::TokioIo;
+use tokio::net::UnixStream;
+
+#[derive(Debug, thiserror::Error)]
+pub enum GetError {
+    #[error("failed to connect to unix socket")]
+    SocketConnect(#[source] std::io::Error),
+    #[error("failed to complete http handshake")]
+    HttpHandshake(#[source] hyper::Error),
+    #[error("given key-value key was invalid")]
+    InvalidKey(#[source] serde_urlencoded::ser::Error),
+    #[error("failed to create request url")]
+    InvalidUrl(#[source] hyper::http::Error),
+    #[error("failed to build http request")]
+    BuildRequest(#[source] hyper::http::Error),
+    #[error("failed to send http request")]
+    SendRequest(#[source] hyper::Error),
+    #[error("http request returned with a failed response: {0}")]
+    FailedResponse(StatusCode),
+    #[error("failed to collect response body")]
+    CollectBody(#[source] hyper::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum SetError {
+    #[error("failed to connect to unix socket")]
+    SocketConnect(#[source] std::io::Error),
+    #[error("failed to complete http handshake")]
+    HttpHandshake(#[source] hyper::Error),
+    #[error("given key-value key was invalid")]
+    InvalidKey(#[source] serde_urlencoded::ser::Error),
+    #[error("failed to create request url")]
+    InvalidUrl(#[source] hyper::http::Error),
+    #[error("failed to build http request")]
+    BuildRequest(#[source] hyper::http::Error),
+    #[error("failed to send http request")]
+    SendRequest(#[source] hyper::Error),
+    #[error("http request returned with a failed response: {0}")]
+    FailedResponse(StatusCode),
+}
+
+pub struct Client {
+    path: PathBuf,
+}
+
+impl Client {
+    pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, GetError> {
+        let stream = UnixStream::connect(&self.path)
+            .await
+            .map_err(GetError::SocketConnect)?;
+        let io = TokioIo::new(stream);
+
+        let (mut sender, conn) = conn::http1::handshake(io)
+            .await
+            .map_err(GetError::HttpHandshake)?;
+
+        tokio::task::spawn(async move {
+            if let Err(err) = conn.await {
+                tracing::error!("Connection failed: {:?}", err);
+            }
+        });
+
+        let query = write_query(key).map_err(GetError::InvalidKey)?;
+
+        let uri = Uri::builder()
+            .scheme("http")
+            .authority("localhost.invalid-tld")
+            .path_and_query(query)
+            .build()
+            .map_err(GetError::InvalidUrl)?;
+
+        let req = Request::builder()
+            .method(Method::GET)
+            .uri(uri)
+            .body(Full::new(Bytes::from_static(&[])))
+            .map_err(GetError::BuildRequest)?;
+
+        let res = sender
+            .send_request(req)
+            .await
+            .map_err(GetError::SendRequest)?;
+
+        if matches!(res.status(), StatusCode::BAD_REQUEST) {
+            return Err(GetError::FailedResponse(res.status()));
+        }
+        if matches!(res.status(), StatusCode::NOT_FOUND) {
+            return Ok(None);
+        }
+
+        let body = res
+            .into_body()
+            .collect()
+            .await
+            .map_err(GetError::CollectBody)?
+            .to_bytes();
+
+        Ok(Some(body.to_vec()))
+    }
+
+    pub async fn set(&self, key: &str, value: &[u8]) -> Result<(), SetError> {
+        let stream = UnixStream::connect(&self.path)
+            .await
+            .map_err(SetError::SocketConnect)?;
+        let io = TokioIo::new(stream);
+
+        let (mut sender, conn) = conn::http1::handshake(io)
+            .await
+            .map_err(SetError::HttpHandshake)?;
+
+        tokio::task::spawn(async move {
+            if let Err(err) = conn.await {
+                tracing::error!("Connection failed: {:?}", err);
+            }
+        });
+
+        let query = write_query(key).map_err(SetError::InvalidKey)?;
+
+        let uri = Uri::builder()
+            .scheme("http")
+            .authority("localhost.invalid-tld")
+            .path_and_query(query)
+            .build()
+            .map_err(SetError::BuildRequest)?;
+
+        let body = Full::new(Bytes::from(value.to_vec()));
+
+        let req = Request::builder()
+            .method(Method::POST)
+            .uri(uri)
+            .body(body)
+            .map_err(SetError::BuildRequest)?;
+
+        let res = sender
+            .send_request(req)
+            .await
+            .map_err(SetError::SendRequest)?;
+
+        if matches!(res.status(), StatusCode::BAD_REQUEST) {
+            return Err(SetError::FailedResponse(res.status()));
+        }
+
+        Ok(())
+    }
+}
+
+impl Default for Client {
+    fn default() -> Self {
+        Self {
+            path: PathBuf::from("/tmp/last-sky.sock"),
+        }
+    }
+}
+
+#[inline]
+fn write_query(key: &str) -> Result<String, serde_urlencoded::ser::Error> {
+    #[derive(serde::Serialize)]
+    struct Key<'k> {
+        key: &'k str,
+    }
+
+    let mut output = "/?".to_string();
+
+    let mut encoder = form_urlencoded::Serializer::new(&mut output);
+    let serializer = serde_urlencoded::Serializer::new(&mut encoder);
+
+    <Key<'_> as serde::Serialize>::serialize(&Key { key }, serializer)?;
+
+    Ok(output)
+}
diff --git a/server/Cargo.toml b/server/Cargo.toml
new file mode 100644
index 0000000..9e4fc36
--- /dev/null
+++ b/server/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "afterimage-server"
+version = "0.1.0"
+edition = "2024"
+
+workspace = ".."
+
+[dependencies]
+ahash.workspace = true
+dashmap.workspace = true
+form_urlencoded.workspace = true
+http-body-util.workspace = true
+hyper-util.workspace = true
+hyper.workspace = true
+serde_path_to_error.workspace = true
+serde_urlencoded.workspace = true
+serde.workspace = true
+tokio.workspace = true
+tracing-subscriber.workspace = true
+tracing.workspace = true
diff --git a/server/src/main.rs b/server/src/main.rs
new file mode 100644
index 0000000..e2c6464
--- /dev/null
+++ b/server/src/main.rs
@@ -0,0 +1,132 @@
+use std::{path::PathBuf, sync::Arc};
+
+use dashmap::DashMap;
+use http_body_util::{BodyExt as _, Full};
+use hyper::{
+    Method, Request, Response, StatusCode,
+    body::{Bytes, Incoming},
+    server::conn::http1,
+    service::service_fn,
+};
+use hyper_util::rt::{TokioIo, TokioTimer};
+use tokio::net::UnixListener;
+use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
+
+#[derive(Clone)]
+struct Data {
+    map: Arc<DashMap<String, Vec<u8>, ahash::RandomState>>,
+}
+
+#[tokio::main]
+async fn main() {
+    tracing_subscriber::registry()
+        .with(
+            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
+                concat!(env!("CARGO_CRATE_NAME"), "=debug,tower_http=warn,axum=warn").into()
+            }),
+        )
+        .with(tracing_subscriber::fmt::layer().without_time())
+        .init();
+
+    let data = Data {
+        map: Arc::new(DashMap::with_hasher(ahash::RandomState::new())),
+    };
+
+    let path = PathBuf::from("/tmp/afterimage.sock");
+    let _ = tokio::fs::remove_file(&path).await;
+    tokio::fs::create_dir_all(path.parent().unwrap())
+        .await
+        .unwrap();
+
+    let uds = UnixListener::bind(path).expect("Failed to bind UNIX socket");
+
+    loop {
+        let (stream, _) = match uds.accept().await {
+            Ok(pair) => pair,
+            Err(err) => {
+                tracing::warn!(err=?err, "failed to accept unix socket connection");
+
+                continue;
+            }
+        };
+
+        let io = TokioIo::new(stream);
+
+        let service = service_fn({
+            let data = data.clone();
+
+            move |req| {
+                let data = data.clone();
+
+                handle(req, data)
+            }
+        });
+
+        tokio::task::spawn(async move {
+            if let Err(err) = http1::Builder::new()
+                .timer(TokioTimer::new())
+                .serve_connection(io, service)
+                .await
+            {
+                tracing::error!("Error serving connection: {:?}", err);
+            }
+        });
+    }
+}
+
+#[inline]
+async fn handle(req: Request<Incoming>, data: Data) -> Result<Response<Full<Bytes>>, hyper::Error> {
+    let method = req.method();
+    let Some(query) = req.uri().query() else {
+        return Ok(response(StatusCode::BAD_REQUEST, empty()));
+    };
+    let Some(key) = parse_query(query) else {
+        return Ok(response(StatusCode::BAD_REQUEST, empty()));
+    };
+
+    let res = match *method {
+        Method::GET => data.map.get(&key).map_or_else(
+            || response(StatusCode::NOT_FOUND, empty()),
+            |bytes| response(StatusCode::OK, full(&bytes[..])),
+        ),
+        Method::POST => {
+            let bytes = req.collect().await?.to_bytes();
+
+            data.map.insert(key, bytes.to_vec());
+
+            response(StatusCode::OK, full(&bytes[..]))
+        }
+        _ => response(StatusCode::BAD_REQUEST, empty()),
+    };
+
+    Ok(res)
+}
+
+#[inline]
+fn parse_query(query: &str) -> Option<String> {
+    #[derive(serde::Deserialize)]
+    struct Key {
+        key: String,
+    }
+
+    serde_path_to_error::deserialize(serde_urlencoded::Deserializer::new(form_urlencoded::parse(
+        query.as_bytes(),
+    )))
+    .ok()
+    .map(|key: Key| key.key)
+}
+
+#[inline]
+fn response(status: StatusCode, body: Full<Bytes>) -> Response<Full<Bytes>> {
+    Response::builder().status(status).body(body).unwrap()
+}
+
+#[inline]
+fn empty() -> Full<Bytes> {
+    Full::new(Bytes::from_static(&[]))
+}
+
+#[inline]
+fn full(bytes: &[u8]) -> Full<Bytes> {
+    Full::new(Bytes::from(bytes.to_vec()))
+}