next-gen web UI framework for Go

write Go to change HTML on the go

as explicit as it gets

directly update HTML as UI events occurs

← Click

// composable ui fragment
type Counter struct {
    // dynamic element
	node  doors.Node
    // some state
	count int
}

// fragment render function
templ (c *Counter) Render() {
    // render dynamic element
    @c.node {
        // initial content
        ← Click
    }
    // button with attribute constructor
    <button { doors.A(ctx, doors.AClick {
            // click handler, REvent - http request wrapper with deserialized event
            On: func(ctx context.Context, _ doors.REvent[doors.PointerEvent]) bool {
                c.count += 1
                // update node content
                c.node.Update(ctx, c.display())
                // not done, keep hook active
                return false
            },
    })... }>
        Click Me!
    </button>
}

templ (c *Counter) display() {
	Clicked { fmt.Sprint(c.count) } time(s)!
}

superior routing

reactive, typed and declarative

Just home page
// *this* page path is deserialized into struct (model)
type Path struct {
    // path option 1: root
	Main  bool `path:""`
    // path option 2: /show + parameter value, save to Id 
	Show   bool `path:"show/:Id"`
	Id     int
    // query parameter
	Filter string `query:"filter"`
}
// beam - source of path model updates 
templ Demo(beam doors.Beam[Path]) {
    // subscribe to path changes 
	@doors.Sub(beam, func(p Path) templ.Component {
        // if option 1 is active - render home
		if p.Main {
			return home()
        // if option 2 is active - render show
		} else if p.Show {
            return show(p.Id, p.Filter)
		}
        // render nothing otherwise
		return nil
	})
	// setup active link visualization
	{{active := d.Active{
		// Match full path (it's by default, here just for demo)
		PathMatcher: d.ActivePathFull(),
		// Match all query params (it's by default, here just for demo)
		QueryMatcher: d.ActiveQueryAll(),
		// Indicate activity with attribute
		Indicate: []d.Indicate{
			d.IndicateAttr("aria-current", "page"),
		},
	}}}
    // prepare model to form href attr
	{{ model := common.MainPath { Main: true } }}
	// construct link with model and active link setup
	<a { d.A(ctx, d.AHref { Active: active, Model: model })... }>
		Main 
	</a>
	// prepare model for another path variant
	{{ model = common.MainPath { Show: true, Id: 9, Filter: "color" } }}
	<a { d.A(ctx, d.AHref { Active: active, Model: model } )... }>
		Show #9
	</a>
}

templ home() {
	Just home page
}

templ show(id int, filter string) {
	id: { fmt.Sprint(id) }, filter: { filter }
}

it's time to switch branches

In programming, a bad decision is like node_modules directory — it never stops growing.

Sometimes you have to step back to realize you’ve been solving problems that shouldn’t have existed in the first place.

I couldn’t shake that feeling — whether I was using frontend frameworks or dealing with sophisticated API access control schemas on the server side.

Now, after years of building and learning, I finally have the experience — and resources — to offer an alternative.

— founder

native and friendly form handeling

use deserialization or deal with HTTP request yourself

Submit Form
 // form data for parsing
type FormData struct {
	Name  string `form:"name"`
	Email string `form:"email"`
}

templ Demo() {
    // dynamic element
	{{ var node doors.Node }}
    // render dynamic element with initial content
    @node {
        Submit Form
    }
	// form with submit handler (you can also use doors.ARawSubmit if you want to do form parsing/reading yourself)
	<form { doors.A(ctx, doors.ASubmit[FormData]{
       // block form submission until previous one is processed
       Mode: doors.ModeBlock(),
       // indicate pending form (will be active untill all changes triggered by the hook are applied)
       Indicator: []doors.Indicate{
           // set attribute to #form-submit
           doors.IndicateAttr("#form-submit", "aria-busy", "true"),
           // change content of #form-submit
           doors.IndicateContent("#form-submit", "Simulating delay...",),
       },
       // handle form result
       On: func(ctx context.Context, r doors.RForm[FormData]) bool {
            // wait some time for demo purposes
            <-time.NewTimer(time.Second).C
            // show submitted data
            node.Update(ctx, showData(r.Data()))
            /*
                You can also r.SetCookie(name, value) and r.GetCookie(name) in all hooks
            */
            // not done, keep hook active
            return false
       },
    }) ... }
	>
        <input
            name="name"
            placeholder="Name"
            required=true
        />
        <input
            type="email"
            name="email"
            placeholder="Email"
            required=true
        />
		<button	id="form-submit" role="submit">Submit</button>
	</form>
}

// display form values
templ showData(d FormData) {
	name: { doors.Name }, email: { doors.Email }
}

server-driven for the real world

technology you shouldn’t avoid anymore

⚡ Responsive
doors uses seamless rolling requests with stream body reading and a robust sync protocol.
Unlike common WS or SSE approaches, which are prone to slow disconnect detection, wakeup sloppiness, half-open states, and silent drops.
⛓️‍💥 Unbounded
Advanced concurrency model enables parallel event processing, state propagation, and rendering — without race conditions.
No compromises in favor of internal simplicity.
👫 Straight
Hooks as regular HTTP request handlers, composable front-end style state and components, native routing, seamless JS integration.
Clean combination of SPA and MPA techologies with minimal artifacts.

seamless frontend ↔ backend integration

trigger Go hooks from JS, call JS functions from Go, and pass data effortlessly

/*
    in this demo we will pass data to frontend, 
    then use that data in JS to trigger Go hook 
    in which js code will be called to show alert... 
*/
templ Demo() {
    // generate id
	{{ id := uuid.New().String() }}
    // use generated id 
	<button id={ id }></button>
    // first we save attributes to a variable for readablity
	{{ attrs := doors.A(ctx,
        // pass data to frontend
        doors.AData{
            Value: id,
            Name:  "id",
        },
        // attach hook with int input and int output
        doors.AHook[int, int]{
           // block until previous one is processed
            Mode: doors.ModeBlock(),
            // hook name
            Name: "callback",
            // hook handler
            On: func(ctx context.Context, r doors.RHook[int]) (int, bool) {
                // make "alert" call to the frontend (check call handler in script below)
                doors.Call(ctx, doors.CallConf{
                    Name: "alert",
                    Arg:  fmt.Sprint("Go will multiply: ", r.Data()),
                    /* you can also process js call return value and respond to it */
                })
                // hook response
                return r.Data() * 2, false
            },
        },
    ) }}
    // 
    // convert inline script to script with src  
    // access atributes with $d, don't worry about global scope, and use async
    @doors.Script {
        // use our saved attributes on a js webpage script
        <script { attrs... } />
            // read data "id" from using special $d object (scoped to current script for attrubute reading)
            const id = $d.data("id")
            // use id to get button element
            const button = document.getElementById(id);
            let total = 1;
            button.innerHTML = `multiply ${total} by 2 in Go`
            // attach call with name "alert" (to closest dynamic parent element or root), 
            $d.on("alert", (message) => {
                // just alert whatever go gives
                alert(message)
                /* you can also provide return value here */
            })  /* <- you can also handle Go response to call result (🤯) in third argument*/
            // add plain event listener to our button
            button.addEventListener('click', async () => {
                try {
                   // trigger hook  with the name "callback" and total value as argument
                   total = await $d.hook("callback", total)
                   // update button content with the response
                   button.innerHTML = `multiply ${total} by 2 in Go`
                }catch(e) {
                    // check if hook blocked (we set block mode, so it's ok)
                    if(e.isBlocked()) {
                        console.log("Call blocked")
                        return
                    }
                    throw e
                }
            })
        </script>
    }
}

complete web dev toolkit

and different in a good way

🧬 transperent & lean state managment
  • create state sources and derive new ones from it
  • update or mutate state, all changes will be distributed in a parallel and safe way
  • subscribe to introduce controlled DOM updates
⚡ performant & controlled
  • parallel rendering & event handeling
  • controlled resource consumption with a goroutine pool and instance limit per session
  • extra-thin client side
✨ UX-friendly
  • indication for pending operations and active links
  • debouncing and other event handeling modes
  • integration of js apps and modules for UI-heavy stuff
🧩 server driven superpowers
  • no exposed endpoints you don't have to deal endpoinds at all
  • always SSR hydration hell is in the past
  • unique page instance users can only interact with what they see
  • minimal js zero NPM dependencies
📦 builds & serves
  • build your js/ts modules
  • cook import map & CSP headers
  • host files in protected or public modes
  • transform inline scripts and styles to external
🚀 set up & run with no hassle
  • it's just router - use it with standart Go http server
  • build with awesome templating solution - templ
  • batteries included bundle and build js/ts without extra tools

a lifetime affordable license

to ensure independence and sustainability

  • not backed by big company or VC funding
  • not relying on spare-time maintanance
  • here to give you the best tool

don't miss

tell me that you are interested, get email on beta launch - no subsription