Skip to content

QML / one toolchain / zero third party servers

Build complete web systems with QML, with no third party servers to stand up.

SynQt (pronounced synced) is built from entities: a browser client, a web edge, a database, and whatever else your system needs, each its own binary, sharing one toolchain and one security model.

curl -fsSL https://get.synqt.org/install.sh | sh

irm https://get.synqt.org/install.ps1 | iex

pipx install synqt

SynQt

Why SynQt

  • One language, front to back

    Write the UI and the server side logic in QML. The boundary between any two components is a set of typed connect points, named and access controlled by configuration.

  • Live by default

    A value that updates across every browser the instant it changes, with no manual wiring and no client side polling, is a few lines of QML.

  • Batteries included, no third party servers

    Add a database, cache, document store, gateway, or jobs runner as a first party entity. Back it with an embedded engine, or mask PostgreSQL, MongoDB, or Redis behind it with one config value.

  • Web and desktop, one codebase

    The client is a Qt app. Ship it to the browser as WebAssembly and, from the same QML, as a native app for Windows, macOS, and Linux, against the same edge and the same security model.

  • Secure at every link

    Every link is encrypted and authenticated from the first build. There is no plaintext connection type to reach for by mistake, only the one every entity already speaks.

  • One click, one trace

    Add a monitor and every entity reports to it, so a click in the browser becomes one trace running through each entity it touched. Nothing is recorded until you add one, and turning a category up during an incident is a restart, not a rebuild.

A closer look

Contracts and connect points

Two entities talk through a connect point: a named, typed, live object one entity owns and the others see a live copy of. Properties and signals flow from the owner out to every consumer; slots flow the other way, and the owner always decides.

Read the programming model →

Security by default

TLS everywhere, mutual TLS between entities, a deny by default topology, and data minimization built into the contract format itself. There is no insecure middle state a project can accidentally ship in.

Read the security model →

One toolchain

The synqt CLI installs and pins the exact Qt and Emscripten versions your project needs, builds every entity, native and WebAssembly alike, and runs them all together with file watching and hot reload.

Read the build system and CLI guide →

What it looks like

A chat room, because everyone already knows what one does. Somebody types a line and it appears in every window that has the room open, including the ones on other machines. That is the part SynQt is for, and below is all of it.

A finished system is a small mesh of entities. The drawing below is that mesh, drawn by the design editor from this very project: three boxes saying which side of the wire each entity is on, a shape per entity, and on every line the contract the two ends share. Only the web edge faces the internet; everything else sits in the mesh box and is reachable only by the entities you allow.

A visitor who has signed in as nobody gets a sign-in page, and the room is absent rather than hidden: the point that carries it is gated scope: user, so their session never acquires it and there is nothing on their side to get past. Signing in fills the same window with the room. A moderator gets one member more than everybody else, erase, and gets it because the contract says so and not because the client decided to offer it.

Seven files are the whole system: one configuration file, which says what crosses each link, one QML file per entity, two more the client's window opens, and the table the database keeps the messages in. Hover (or focus) an entity to read the file it is, or the mark on a line to read the block that says what crosses it, and the card that opens says the rest. The project tree under the drawing opens the same files, and one stays open until you move to another. The database opens two, its QML and the table that QML queries, since neither says much without the other. The files carry no explanatory comments: a line with something to say about itself is marked down its left instead, and hovering it says the thing. A line that ends in an arrow opens the page covering it, whether that is a page of this guide or the class in the C++ reference.

The button under it opens this same drawing in the online designer, which runs in the browser with nothing installed. It is the same code that drew it here, so nothing is lost on the way: pull the mesh apart there, add an entity, and export the result as a project.

Project tree
  • synqt.yaml
  • client
  • app
  • Main.qml
  • User.qml
  • Admin.qml
  • web
  • edge
  • Edge.qml
  • db/relational
  • store
  • Store.qml
  • schema.sql
configurationsynqt.yaml
project:
  name: chat
  qt_version: 6.12.0

scopes: { order: [anonymous, user, admin], default: anonymous }

identity:
  providers: [{ name: github, client_id: ..., client_secret: env:SECRET }]
  mapping: web/edge/identity/map.qml

entities:
  - { name: app, type: client }
  - name: edge
    type: web_edge
    identity: true
    public: { port: 8443, sync_route: /sync }
  - { name: store, type: relational, provider: { name: sqlite } }

connect_points:
  - owner: edge
    consumers: [app]
    scope: user
    export: |
      model messages(int id, string[40] who, string[280] body, bool staff)
      slot say(string[280] body)
      <admin> slot erase(int id)
  - owner: store
    consumers: [edge]
    export: |
      prop var[24000] lines
      slot say(string[40] who, string[280] body, bool staff)
      slot erase(int id)
appclient/app/Main.qml
import SynQt
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    id: window

    visible: true
    title: qsTr("The chat room")

    ColumnLayout {
        anchors.centerIn: parent
        visible: !Session.hasScope("user")
        spacing: 24

        Label {
            Layout.alignment: Qt.AlignHCenter
            font.pixelSize: 32
            text: qsTr("One room. Everybody in it sees the same thing.")
        }

        Button {
            Layout.alignment: Qt.AlignHCenter
            text: qsTr("Sign in with GitHub")
            onClicked: Session.login()
        }
    }

    User {
        anchors.fill: parent
        visible: Session.hasScope("user")
    }
}
the roomclient/app/User.qml
import SynQt
import QtQuick.Controls
import QtQuick.Layouts

ColumnLayout {
    ListView {
        id: messages

        Layout.fillHeight: true
        Layout.fillWidth: true
        clip: true
        model: Server.messages

        delegate: Item {
            id: line

            required property var model

            width: messages.width
            height: 26

            Label {
                x: 8
                width: 132
                height: parent.height
                verticalAlignment: Text.AlignVCenter
                elide: Text.ElideRight
                color: line.model.staff ? "#d0342c" : line.palette.windowText
                font.bold: line.model.staff
                text: line.model.who
            }

            Label {
                x: 148
                width: parent.width - 148 - 88
                height: parent.height
                verticalAlignment: Text.AlignVCenter
                elide: Text.ElideRight
                text: line.model.body
            }

            Admin {
                x: parent.width - 84
                y: 1
                width: 76
                height: parent.height - 2
                messageId: line.model.id
            }
        }
    }

    TextField {
        id: draft

        Layout.fillWidth: true
        placeholderText: qsTr("Say something")
        onAccepted: {
            Server.say(draft.text);
            draft.clear();
        }
    }
}
the moderator's buttonclient/app/Admin.qml
import SynQt
import QtQuick.Controls

Button {
    id: control

    required property int messageId

    visible: Session.hasScope("admin")
    text: qsTr("Erase")
    onClicked: Server.erase(control.messageId)
}
edgeweb/edge/Edge.qml
import SynQt

Edge {
    messagesRows: Store.lines

    function say(body) {
        Store.say(Caller.identity.login, body, Caller.hasScope("admin"));
    }

    function erase(id) {
        Store.erase(id);
    }
}
databasedb/relational/store/Store.qml
import SynQt

Store {
    id: log

    function say(who, body, staff) {
        Db.exec("INSERT INTO messages (who, body, staff, said_at) "
                + "VALUES (?, ?, ?, datetime('now'))",
                [who, body, staff ? 1 : 0]);
        log.refresh();
    }

    function erase(id) {
        Db.exec("UPDATE messages SET body = 'deleted by a moderator' WHERE id = ?",
                [id]);
        log.refresh();
    }

    function refresh() {
        const rows = Db.query("SELECT id, who, body, staff FROM messages "
                              + "ORDER BY id DESC LIMIT 50");
        log.lines = rows.map(row => ({ id: row.id, who: row.who, body: row.body,
                                       staff: row.staff !== 0 }));
    }

    lines: []

    Component.onCompleted: log.refresh()
}
tabledb/relational/store/schema.sql
CREATE TABLE IF NOT EXISTS messages (
    id      INTEGER PRIMARY KEY AUTOINCREMENT,
    who     TEXT NOT NULL,
    body    TEXT NOT NULL,
    staff   INTEGER NOT NULL DEFAULT 0,
    said_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS messages_by_time
    ON messages (said_at);

Hover a marked line for what it does. A line ending in an arrow opens the page covering it.

Where to go next

  • Getting started: install synqt and run your first project.
  • Framework: the full reference, from the entity model to the security design.
  • Examples: complete worked systems.
  • Contributing: the codebase map, for working on SynQt itself.
  • C++ reference: the generated class and member reference for the runtime.