Tired of GUI builders? I made a code-first alternative to Retool

Hey DEV community! 👋

Have you ever found yourself spending hours dragging and dropping components in a GUI builder when all you wanted was to quickly create an admin panel? That was me, and I got tired of it. So I built something different.

The drag-and-drop frustration

As a backend developer, I love working with databases, implementing business logic, and building APIs. But every time I needed to create an internal tool or admin panel, I’d struggle with GUI-based builders like Retool.

Don’t get me wrong – Retool is powerful. But I kept running into issues:

  • 🤦‍♂️ Moving components in the GUI would mysteriously break connections
  • 😵 Reviewing changes in pull requests was nearly impossible
  • 🤔 Teaching new team members the proprietary UI was time-consuming
  • 🤖 AI coding assistants couldn’t help with GUI-based configurations

Plus, there was always this disconnect between my backend code and the frontend. I’d write a function in Go or TypeScript, then manually wire it up to a UI component through a confusing interface.

I just wanted to connect my backend functions directly to a UI!

The code-first dream

What if we could do this instead:

func usersPage(ui sourcetool.UIBuilder) error {
    // Simple UI definition in Go
    ui.Markdown("## Users")

    // Input directly connected to your function
    name := ui.TextInput("Name", textinput.WithPlaceholder("filter by name"))

    // Call your existing backend function
    users, err := listUsers(name)
    if err != nil {
        return err
    }

    // Display the results
    ui.Table(users)
    return nil
}

That’s it! No frontend code, no separate repository, no complex build setup. Just your backend code with some UI definitions.

This is exactly what Sourcetool does.

How it actually works

Sourcetool has a simple architecture with three main players:

  1. Your backend server (where your Go code runs)
  2. A Sourcetool server (handles auth and acts as a WebSocket bridge)
  3. The user’s browser (where the UI renders)

When a user interacts with the UI (clicks a button, types in a field), that event travels through WebSockets to your backend, where your code handles it. The UI then updates in real-time based on your response.

It takes just a few lines to set up:

func main() {
    st := sourcetool.New(&sourcetool.Config{
        APIKey:   "YOUR_API_KEY",
        Endpoint: "wss://your-sourcetool-instance",
    })

    // Register your pages
    st.Page("/users", "Users", usersPage)
    st.Page("/products", "Products", productsPage)

    // Start the server
    if err := st.Listen(); err != nil {
        log.Fatal(err)
    }
}

Perfect for the AI coding era

With tools like GitHub Copilot, ChatGPT, and Cursor changing how we write code, GUI builders feel increasingly outdated.

I can ask an AI to:

  • “Add a form with name and email fields that calls the createUser function”
  • “Add validation to this form”
  • “Create a new page for displaying analytics”

And since it’s all just code, the AI can help directly – no screenshots or complex instructions needed!

The advantages over GUI builders

Here’s why a code-first approach works better for me:

  1. Git-friendly – Review PRs, track changes, and understand the history of your app
  2. Type-safe – Catch errors at compile time, not when users are trying to use your app
  3. Familiar tools – Use your existing IDE, linters, and code review workflows
  4. AI-friendly – Let AI assistants help you build and modify your tools
  5. No context switching – Stay in your backend language rather than jumping between environments

Current status

Right now, Sourcetool supports Go with TypeScript and Python SDKs coming soon. It’s entirely open-source under the Apache 2.0 license, so you can self-host and use it for free.

Here’s a quick example of creating a form:

func createUserPage(ui sourcetool.UIBuilder) error {
    formUI, submitted := ui.Form("Create", form.WithClearOnSubmit(true))

    name := formUI.TextInput("Name", textinput.WithRequired(true))
    email := formUI.TextInput("Email", textinput.WithRequired(true))

    if submitted {
        _ = createUser(name, email)
    }
    return nil
}

Join the community!

I’ve open-sourced Sourcetool on GitHub and would love your feedback. It’s still early days, but if you share my frustrations with GUI builders, give it a try!

If you find this interesting, a GitHub star ⭐ would really help motivate further development! You can also reach out via Twitter/X DMs.

What do you think?

I’d love to hear your thoughts! Do you prefer GUI builders or code-first approaches? Have you experienced similar frustrations? Let me know in the comments below!

P.S. If you’re curious about how we built the backend-to-frontend bridge, check out this deep dive into WebSocket architecture.

Leave a Reply