Authors: Nyx 🦞 + Kiro 🐺 Date: 2026-04-27 Status: DRAFT — Awaiting Fabian's approval
NyxCode v0.40 is a powerful DSL but NOT a programming language. Evidence:
-
Every UI pattern = new compiler code. Rating, wizard, toggle, choice = 4 TypeScript functions in compiler.ts. Next app needs datepicker? Another compiler function. Chatbubble? Another one. This doesn't scale.
-
Frontend logic is limited.
on:clickcan dosetandpush, but not conditionals, loops, API calls, or multi-step logic. Real apps need real logic. -
Components are shallow. No typed props, no named slots, no lifecycle, no event forwarding. Can't build a design system.
-
Backend and frontend are separate languages. Backend has
fn,match,try/catch,each,while. Frontend has...setandpush. They should be ONE language. -
No standard library. Everything is compiler-hardcoded or raw HTML/JS.
One .nyx file = complete fullstack app. ANY web app. AI writes it in 3.5x fewer tokens than Next.js.
NyxCode v0.50 should be able to build:
- ✅ Typeform clone (wizard, conditional logic, ratings)
- ✅ Trello clone (drag-drop, lists, cards, real-time)
- ✅ Chat app (WebSocket, messages, typing indicators)
- ✅ Dashboard (charts, tables, filters, pagination)
- ✅ E-commerce (cart, checkout, payments)
- ✅ Blog/CMS (CRUD, rich text, image upload)
- ✅ Auth flows (login, register, forgot password, OAuth)
- Token efficiency.
let x = 0notconst [x, setX] = useState(0). Every character earns its place. - One language, everywhere. Same syntax in backend
api {}, frontendpage {}, and components. - Compiler knows primitives. Elements like
div,button,input. Everything else is NyxCode components. - Explicit > magic. You see what's reactive. No hidden re-renders.
- Progressive complexity. Simple things are simple. Complex things are possible.
let count = 0 # mutable, reactive in pages
let name = "Nyx" # string
let items = ["a", "b", "c"] # array
let config = { theme: "dark" } # object
const API_URL = "https://..." # immutable, compile-time
# Arithmetic
x + y x - y x * y x / y x % y
# Comparison
x == y x != y x > y x < y x >= y x <= y
# Logic
x and y x or y not x
# String interpolation
"Hello {name}, you have {count} items"
# Ternary
x > 0 ? "positive" : "negative"
# Nullish
user.name ?? "Anonymous"
# Member access
user.profile.avatar
items[0]
config["theme"]
fn add(a, b) { return a + b }
fn greet(name = "world") { return "Hello {name}" }
# Arrow-style (for inline)
fn double(x) { x * 2 }
# Multi-line
fn processOrder(order) {
let total = 0
each order.items -> item {
set total = total + item.price * item.qty
}
return { total, tax: total * 0.19 }
}
# Conditionals
if count > 0 {
p "Has items"
} else if count == 0 {
p "Empty"
} else {
p "Negative?!"
}
# Pattern matching
match status {
"active" -> badge "Active" color=green
"pending" -> badge "Pending" color=yellow
"banned" -> badge "Banned" color=red
_ -> badge "Unknown" color=gray
}
# Loops
each items -> item { div "{item.name}" }
each items -> item, index { div "#{index}: {item.name}" }
for i in 0..10 { span "{i}" }
while condition { ... }
# Error handling
try {
let data = fetch GET "/api/data"
} catch err {
toast error "Failed: {err.message}"
}
# Array
items.push("new")
items.pop()
items.shift()
items.filter(fn(item) { item.active })
items.map(fn(item) { item.name })
items.find(fn(item) { item.id == targetId })
items.sort(fn(a, b) { a.price - b.price })
items.length
# Object
keys(config)
values(config)
entries(config)
merge(defaults, overrides)
page / {
let count = 0 # → Signal. Compiler wraps in createSignal()
let name = "" # → Signal
let items = [] # → Reactive Array (tracks mutations)
p "Count: {count}" # → Auto-subscribes, updates on change
input value=".name" # → Two-way binding
}
page / {
let price = 100
let qty = 1
let total = { price * qty } # → Computed. Re-evaluates when deps change.
let formatted = { "${total.toFixed(2)}€" }
p "Total: {formatted}"
}
page / {
let query = ""
effect {
if query.length > 2 {
let results = fetch GET "/api/search?q={query}"
set searchResults = results
}
}
}
page / {
let todos = []
button "Add" on:click {
push todos { text: "New todo", done: false }
}
each todos -> todo, i {
div {
checkbox checked=".todos[{i}].done"
span "{todo.text}"
button "×" on:click { remove todos i }
}
}
}
store cart {
items: []
total: { items.reduce(fn(sum, i) { sum + i.price * i.qty }, 0) }
fn add(product) {
let existing = items.find(fn(i) { i.id == product.id })
if existing {
set existing.qty = existing.qty + 1
} else {
push items { ...product, qty: 1 }
}
}
fn remove(id) {
set items = items.filter(fn(i) { i.id != id })
}
fn clear() { set items = [] }
}
component Button(label: string, variant = "primary", disabled = false) {
button class="btn btn-{variant}" disabled=disabled on:click { emit click } {
"{label}"
}
}
component Card(title: string) {
div class="card" {
div class="card-header" { h3 "{title}" }
div class="card-body" { slot }
}
}
page / {
use Button("Click me", variant="danger") on:click { set count = count + 1 }
use Card("My Card") {
p "This goes into the slot"
p "Multiple children work"
}
}
component Layout(title: string) {
div class="layout" {
header { slot header }
main { slot }
footer { slot footer }
}
}
page / {
use Layout("App") {
slot header { nav { link "/" "Home", link "/about" "About" } }
p "Main content here"
slot footer { p "© 2026" }
}
}
component SearchInput(placeholder = "Search...") {
let query = ""
input value=".query" placeholder=placeholder on:input {
emit search query
}
}
page / {
let results = []
use SearchInput on:search -> q {
set results = fetch GET "/api/search?q={q}"
}
}
# These ship as stdlib/rating.nyx, stdlib/wizard.nyx, etc.
# Users can import or override them.
component Rating(max = 5, value = 0) {
let hovered = -1
div class="rating" {
for i in 1..max+1 {
span class="star {i <= (hovered >= 0 ? hovered : value) ? 'filled' : ''}"
on:mouseenter { set hovered = i }
on:mouseleave { set hovered = -1 }
on:click { emit change i }
"★"
}
}
}
component Wizard() {
let step = 0
let steps = children.length
div class="wizard" {
div class="progress" {
for i in 0..steps {
div class="dot {i <= step ? 'active' : ''}"
}
}
each children -> child, i {
if i == step { child }
}
div class="nav" {
button "← Back" disabled={step == 0} on:click { set step = step - 1 }
button "{step == steps - 1 ? 'Submit' : 'Next →'}" on:click {
if step < steps - 1 { set step = step + 1 }
else { emit complete }
}
}
}
}
button "Save" on:click {
set saving = true
try {
let result = fetch POST "/api/save" { title, body }
toast success "Saved!"
set saving = false
navigate "/posts/{result.id}"
} catch err {
toast error "Failed: {err.message}"
set saving = false
}
}
input on:input { set query = event.target.value }
input on:keydown.enter { call submitForm() }
div on:scroll { if event.scrollY > 100 { set showNav = true } }
form on:submit.prevent { call handleSubmit() }
div on:click.outside { set menuOpen = false }
page / {
on:key.ctrl+s { call save() }
on:key.escape { set modalOpen = false }
on:key.ctrl+k { set searchOpen = true }
}
api /posts {
fn list(page = 1, limit = 20) {
let posts = query "SELECT * FROM posts LIMIT :limit OFFSET :offset" {
limit, offset: (page - 1) * limit
}
respond posts
}
fn create(title, body) auth {
let post = query "INSERT INTO posts (title, body, author_id) VALUES (:title, :body, :uid) RETURNING *" {
title, body, uid: auth.id
}
respond post status=201
}
}
page /posts {
let posts = fetch GET "/api/posts" # auto-loading state
let loading = posts.loading # boolean
let error = posts.error # null or error
if loading { spinner "Loading..." }
if error { alert "Error: {error.message}" }
each posts.data -> post {
use PostCard(post)
}
}
page /posts/new {
let title = ""
let body = ""
form on:submit.prevent {
let result = fetch POST "/api/posts" { title, body }
if result.ok {
navigate "/posts/{result.data.id}"
}
} {
input value=".title" placeholder="Title"
textarea value=".body" placeholder="Write..."
button "Publish" type="submit"
}
}
# Server
socket /chat {
on connect -> client {
broadcast "{client.name} joined"
}
on message -> data, client {
broadcast data
}
on disconnect -> client {
broadcast "{client.name} left"
}
}
# Client
page /chat {
let messages = []
let input = ""
connect /chat -> msg {
push messages msg
}
each messages -> msg { p "{msg}" }
input value=".input" on:keydown.enter {
send input
set input = ""
}
}
# Server
stream /notifications auth {
each event from notifications.where(user_id: auth.id) {
yield event
}
}
# Client
page /dashboard {
subscribe /notifications -> event {
push alerts event
toast info "{event.title}"
}
}
page / { h1 "Home" }
page /about { h1 "About" }
page /pricing { h1 "Pricing" }
page /posts/:id {
let post = fetch GET "/api/posts/{params.id}"
h1 "{post.data.title}"
p "{post.data.body}"
}
page /users/:userId/posts/:postId {
# params.userId, params.postId available
}
page /dashboard auth {
# Redirects to /login if no token
h1 "Welcome, {auth.user.name}"
}
page /admin auth role="admin" {
# Redirects if not admin
}
link "/posts" "All Posts" # auto SPA navigation
button "Go" on:click { navigate "/dashboard" }
# navigate(path) — programmatic routing
# navigate(-1) — go back
layout /app {
nav { link "/" "Home", link "/about" "About" }
main { slot }
footer { p "© 2026" }
}
page /app/ layout=/app { h1 "Dashboard" }
page /app/settings layout=/app { h1 "Settings" }
page /register {
let email = ""
let password = ""
let errors = {}
form on:submit.prevent {
set errors = validate {
email: required email
password: required min=8
}
if not errors {
fetch POST "/api/register" { email, password }
}
} {
input type="email" value=".email" error=".errors.email"
input type="password" value=".password" error=".errors.password"
button "Register" type="submit"
}
}
validate {
name: required min=2 max=50
email: required email
age: required number min=18
url: url
password: required min=8 matches="^(?=.*[A-Z])(?=.*[0-9])"
confirm: required equals=password
}
Keep everything that works:
- CSS shorthands (
bg,p,m,r,fs,fw, etc.) - Tailwind classes (
class="flex items-center gap-4") - Theme tokens (
theme { colors { primary #667eea } }) - Presets (
preset card { bg white, r 12px, p 2rem }) - Dark mode (
@dark { bg #0a0a12 }) - Responsive (
@md { fs 1.5rem }) - Animations (
@keyframes fadeIn { ... })
Keep and extend:
table— auto CRUDauth— JWT authapi— custom endpointspipe— data pipelinesaction— reusable functionsemail— email sendinguse— package system
job cleanupExpired every="1h" {
query "DELETE FROM sessions WHERE expires_at < NOW()"
}
job sendDigest every="1d" at="09:00" {
let users = query "SELECT * FROM users WHERE digest = true"
each users -> user {
email user.email "Daily Digest" template="digest" { user }
}
}
storage uploads {
path "/uploads"
max 10mb
allow ["image/*", "application/pdf"]
rename uuid
}
api /avatar auth {
fn upload(file) {
let url = store file in uploads
query "UPDATE users SET avatar = :url WHERE id = :uid" { url, uid: auth.id }
respond { url }
}
}
middleware rateLimit(max = 100, window = "1m") {
# Applied to routes
}
middleware cors(origins = ["*"]) {
# CORS headers
}
api /public use=[cors] { ... }
api /private use=[rateLimit(max=10)] auth { ... }
table posts { title text required, body text, created auto }
auth { user email password, token jwt }
page / {
let posts = fetch GET "/api/posts"
each posts.data -> post {
card { h2 "{post.title}", p "{post.body}" }
}
button "New Post" on:click { navigate "/new" }
}
page /new auth {
let title = ""
let body = ""
form on:submit.prevent {
fetch POST "/api/posts" { title, body }
navigate "/"
} {
input value=".title" placeholder="Title"
textarea value=".body"
button "Publish" type="submit"
}
}
~20 lines. Complete CRUD blog with auth.
- All CSS/styling (shorthands, Tailwind, themes, presets)
- Backend (table, auth, api, pipe, action, email)
- Page structure (page, layout, component)
- All 581 tests (nothing breaks)
letin pages → full Signal semantics (mostly backward compatible)on:click→ multi-statement handlers (superset of current)when→ unifiedif/else(when still works as alias)each→ works on reactive arrays (superset)
- Computed:
let x = { expr } - Effects:
effect { } - Stores:
store name { } - Full
fnin frontend matchin frontendtry/catchin frontendnavigate(),connect,subscribevalidateblocks- Component typed props, named slots, events
- Background
job storageblocks- StdLib: rating.nyx, wizard.nyx, toggle.nyx, etc.
compileRatingInput→ stdlib/rating.nyxcompileToggleInput→ stdlib/toggle.nyxcompileChoiceInput→ stdlib/choice.nyxcompileWizard→ stdlib/wizard.nyxcompileBurgerNav→ stdlib/burger-nav.nyx
- Unified expression engine — same parser for backend + frontend
- Signal runtime — createSignal, createMemo, createEffect
- Multi-statement handlers — on:click { ... multiple statements ... }
- Component system v2 — typed props, named slots, events, children
- Client-side control flow — if/else, match, each, for in pages
- Client-side functions — fn in pages
- Data fetching — fetch with loading/error states
- Navigation — navigate(), dynamic routes
- Forms/validation — validate blocks
- StdLib migration — move hardcoded elements to .nyx files
- Real-time — connect, subscribe
- Backend extensions — job, storage, middleware
This is our North Star. Every decision should pass the test: "Does this make NyxCode a LANGUAGE, or just a bigger template?"
🦞🐺
| # | Question | Decision | Rationale |
|---|---|---|---|
| 1 | Signal vs Static let | let in page {} = Signal, let in api {} = static, const = always static |
Context determines reactivity. Clean, no new keywords. |
| 2 | Computed dependency tracking | Runtime (like SolidJS) | Compile-time misses dynamic refs. __nyx.track() auto-subscribes. |
| 3 | Array reactivity granularity | Phase 1: full re-render. Phase 2+: fine-grained per item. | Pragmatic. Get it working, then optimize. |
| 4 | Nested object reactivity | Shallow (Proxy on top-level) | set user.score = X works. New props need set user = {...user, new: val}. |
| 5 | let in event handlers |
Always local (not reactive) | Only top-level page let = Signal. Handler let = temp variable. |
| 6 | Effect cleanup | Phase 2 | Start without. Add return { cleanup } when leaks emerge. |
| 7 | Type inference | Dynamic (JS-style) | NyxCode is not TypeScript. Types only on component props. |
| 8 | Migration from v0.40 | Zero breaking changes | createState IS already a signal internally. Rename, don't rewrite. |