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) builds a system from entities: a browser client, a web edge, a database, and whatever else you need. Each entity is its own binary, and all of them share 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 logic in QML. Two entities talk through typed connect points, which the configuration names and guards.

  • Live by default

    A few lines of QML give you a value that updates in every browser the moment it changes, with no wiring to write and nothing polling.

  • Batteries included, no third party servers

    Add a database, a cache, a document store, a gateway or a jobs runner as an entity. It runs an embedded engine, or puts PostgreSQL, MongoDB or Redis behind the same interface with one config value.

  • Web and desktop, one codebase

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

  • Secure at every link

    Every mesh link is mutual TLS unless you opt it into a local socket by name, and a release build refuses to serve the browser without TLS. synqt check flags every exception.

  • One click, one trace

    Add a monitor and every entity reports to it. A click in the browser becomes one trace through every entity it touched. Recording starts when you add a monitor, and raising a category's level during an incident takes a restart, not a rebuild.

A closer look

Contracts and connect points

Two entities talk through a connect point: a typed, live object that one entity owns and the others mirror. Properties and signals flow from the owner to every consumer. Slot calls flow the other way, and the owner decides whether to honor them.

Read the programming model →

Security by default

TLS everywhere, mutual TLS between entities, a topology that denies by default, and a contract format that sends only what it declares. A project has no insecure setting to ship by accident.

Read the security model →

One toolchain

The synqt CLI pins the exact Qt and Emscripten versions your project needs and prints the commands that install them. It builds every entity, native and WebAssembly, and runs them together with file watching and hot reload.

Read the build system and CLI guide →

What it looks like

The example is a chat room. A line typed in one window appears in every window that has the room open, on any machine.

The drawing below is this project, drawn by the design editor's own code. The boxes show which side of the network each entity is on, and each line carries the contract its two ends share. Only the web edge faces the internet. The database sits in the mesh, and only the edge can reach it.

A visitor who has not signed in sees a sign-in page. The room's connect point is gated scope: user, so a signed-out session never acquires it and has nothing to get past. Signing in fills the same window with the room. The sign-in runs on the edge, at the door mark on its rim; hover the mark to read what it does. A moderator gets one extra member, erase, and only the contract can grant it.

Nine files make the whole system: synqt.yaml, which says what crosses each link, one QML file per entity, three more QML files the client's window opens, the edge's sign-in mapping, and the database table.

  • Open a file. Hover or focus an entity, a contract mark, or a row of the project tree. The file stays open until you pick another.
  • List an entity's files. Hover the entity in the drawing. Its files appear under its name.
  • Read a note. A line marked on its left has a note: hover it. A line ending in an arrow links to the page that covers it, in this guide or in the C++ reference.

The button under the drawing opens the same project in the online designer. It runs in the browser with nothing installed and draws with the same code. Move the entities, add one, and export the result as a project.

Project tree
  • synqt.yaml
  • client
  • app
  • Main.qml
  • User.qml
  • Message.qml
  • Admin.qml
  • web
  • edge
  • Edge.qml
  • identity/map.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: Message {
            width: messages.width
        }
    }

    TextField {
        id: draft

        Layout.fillWidth: true
        placeholderText: qsTr("Say something")
        onAccepted: {
            Server.say(draft.text);
            draft.clear();
        }
    }
}
one messageclient/app/Message.qml
import SynQt
import QtQuick.Controls

Item {
    id: line

    required property var model

    height: 26

    Label {
        x: 8
        y: (line.height - height) / 2
        width: 132
        elide: Text.ElideRight
        color: line.model.staff ? "#d0342c" : line.palette.windowText
        text: line.model.who
    }

    Label {
        x: 148
        y: (line.height - height) / 2
        width: line.width - 148 - 88
        elide: Text.ElideRight
        text: line.model.body
    }

    Admin {
        x: line.width - width - 8
        height: line.height
        messageId: line.model.id
    }
}
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);
    }
}
who is a moderatorweb/edge/identity/map.qml
import SynQt

IdentityMapping {
    id: mapping

    readonly property var moderators: ["octocat"]

    function scopeFor(identity): int {
        return mapping.moderators.indexOf(identity.login) >= 0
            ? Scope.Admin : Scope.User;
    }
}
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 to read its note. A line ending in an arrow opens the page that covers 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 systems.
  • Contributing: a map of the codebase, for working on SynQt itself.
  • C++ reference: the class and member reference for the runtime.