Skip to content

Where to go next

You have built a real time application with sign-in and storage, across three entities. Below are five independent recipes to grow it. They skip the steps you already know (creating a file, wiring a connect point, running synqt dev) and show only the new pieces.

Resume the lot in progress after a restart

A restart keeps the Hall of Fame but forgets the bid in progress. Store the current lot too.

Add to the books entity's export: in synqt.yaml:

      slot saveCurrent(string[120] item, int amount, string[80] bidder)
      slot var loadCurrent()    // returns the saved lot, or null if none

Add one row to db/relational/books/schema.sql (a single row table for "the current lot"):

CREATE TABLE IF NOT EXISTS current (
    id     INTEGER PRIMARY KEY CHECK (id = 1),
    item   TEXT NOT NULL,
    amount INTEGER NOT NULL,
    bidder TEXT NOT NULL
);

Add to db/relational/books/Books.qml:

function saveCurrent(item, amount, bidder) {
    Db.exec("INSERT INTO current(id, item, amount, bidder) VALUES(1, ?, ?, ?)"
            + " ON CONFLICT(id) DO UPDATE SET item = excluded.item,"
            + " amount = excluded.amount, bidder = excluded.bidder",
            [item, amount, bidder]);
}

function loadCurrent() {
    const rows = Db.query("SELECT item, amount, bidder FROM current WHERE id = 1");
    return rows.length > 0 ? rows[0] : null;
}

The lot lives in web/edge/Edge.qml, so load and save it there: once at startup for the whole entity, not once per browser. The link to the books entity opens after the edge starts, so the load waits for Books.ready, the same way the Hall of Fame's refresh() does. Call auction.load() in Component.onCompleted and in the Books.onReadyChanged handler, auction.saveNow() at the end of placeBid and closeLot, and add:

property bool loaded: false

// Once: a reconnect keeps the lot the edge is already holding.
function load() {
    if (!Books.ready || auction.loaded) {
        return;
    }
    auction.loaded = true;
    // loadCurrent() returns a value, so it resolves asynchronously.
    Books.loadCurrent().then(saved => {
        if (saved) {
            auction.itemName = saved.item;
            auction.highBid = saved.amount;
            auction.highBidder = saved.bidder;
        }
    });
}

function saveNow() {
    Books.saveCurrent(auction.itemName, auction.highBid, auction.highBidder);
}

Restart, and the lot resumes where it was.

Move to PostgreSQL with one config change

The embedded engine is a good start. To keep the data in a managed PostgreSQL instead, change only the database entity's config. The QML stays the same: the engine is hidden behind the entity, so Db.exec and Db.query work as before.

In synqt.yaml, add a provider section to the books entity that names the engine and its connection:

    provider:
      name: postgres
      host: 127.0.0.1        # a private address, never public
      port: 5432
      database: gavel
      user: gavel
      password: env:DB_PASSWORD   # the value lives in the books entity's .env rather than here
      sslmode: verify-full        # the entity verifies the engine certificate
      ca_cert: certs/db-ca.pem

Put the password in db/relational/books/.env as DB_PASSWORD=..., and run synqt doctor, which checks that your Qt kit has the PostgreSQL driver plugin. That is the whole change. (For a quick local trial against a PostgreSQL without TLS, drop sslmode and ca_cert. SynQt allows that only in dev on localhost, and refuses it in a release build.)

Close each lot automatically on a timer (a jobs entity)

Make it a speed auction where each lot closes after a minute. Add a jobs entity, which runs scheduled work:

synqt add entity ticker --type jobs

The ticker calls closeLot, so it must reach the auction. In synqt.yaml, add it as a consumer of the edge's connect point:

    consumers: [app, ticker]

One detail needs care. closeLot is exported as <admin> slot closeLot(...), and a scope belongs to a user's session. The ticker is an entity with no session, so the gate would refuse it. A member that both users and entities call cannot be gated on a scope, so remove the <admin> in synqt.yaml:

      slot closeLot(string[120] nextItem)

Then decide in the slot, where Caller tells the two kinds of caller apart. Send the rejection only to a user, because emit<Signal> targets a browser session. In web/edge/Edge.qml:

function closeLot(nextItem) {
    const fromTicker = Caller.isEntity && Caller.entity === "ticker";
    if (!fromTicker && !Caller.hasScope("admin")) {
        if (Caller.isUser) {
            Caller.emitBidRejected("Not allowed to close this lot.");
        }
        return;
    }
    // ... close the lot as before
}

This is the trade between the two forms. <scope> is shorter, runs before your code and cannot be forgotten, but it only knows about people. As soon as another entity must reach the same member, the decision moves back into the slot.

Then put the schedule in the ticker's QML file, which synqt add entity scaffolded with an example job. It calls the edge it now consumes, under the edge's capitalized name; an entity has one connect point, so Edge is the whole address:

import SynQt

QtObject {
    id: root

    // One minute per lot. `Jobs` runs it off the request path.
    Component.onCompleted: Jobs.every(60000, function () {
        Edge.closeLot("Next mystery lot");
    })
}

Each lot now closes on its own and records its winner, and the next lot opens.

Give each bidder a private maximum bid

Let a signed-in user set a private maximum that only they can see.

Add two members to the edge's point, gated so only signed-in users have them:

      <user> prop int maxBid
      <user> slot setMax(int amount)

shared: false on the edge entity makes the value private: each bidder gets a Source of their own instead of a mirror of one shared Source:

  - name: edge
    type: web_edge
    shared: false

That line changes where the lot must live. Edge.qml is now created per caller, so the auction would become one lot per bidder. Move the shared part into a pragma Shared file beside it, web/edge/Lot.qml:

pragma Shared

import QtQuick

QtObject {
    id: lot

    property string itemName: "A homemade lasagna, baked fresh this morning"
    property int highBid: 0
    property string highBidder: "nobody yet"
}

Then web/edge/Edge.qml binds the shared members to Lot and keeps the private one:

    itemName: Lot.itemName
    highBid: Lot.highBid
    highBidder: Lot.highBidder

    property int maxBid: 0

    function setMax(amount) {
        auction.maxBid = amount;
    }

placeBid and closeLot write to Lot instead of auction, so one bidder's raise still reaches every session.

In the client, read and set the private value with Server.maxBid and Server.setMax(...). The entity creates a Source per caller, so no shared object exists through which one user could see another's maximum, and the bidder's second tab opens on the maximum they set. A natural next step is to make placeBid raise a user up to their stored maximum automatically.

Pin the rules you checked by hand

Three times in this tutorial you proved a rule by hand: the lower bid the edge refused, the placeBid that failed while signed out, and the closeLot only the auctioneer may call. A check by hand never runs again.

The first rule is in your QML, so a test can pin it. Write tests/tst_Auction.qml:

import QtQuick
import QtTest
import SynQt.Test

TestCase {
    name: "Auction"

    EntityTest {
        id: harness

        source: "../web/edge/Edge.qml"
    }

    SignalSpy {
        id: rejections

        target: harness.subject
        signalName: "bidRejected"
    }

    function init() {
        verify(harness.load(), harness.errorString);
        rejections.clear();
    }

    function test_a_bid_names_the_bidder() {
        harness.callerIsUser("user", { sub: "alice", name: "Alice" });
        harness.subject.placeBid(40);
        compare(harness.subject.highBid, 40);
        compare(harness.subject.highBidder, "Alice");
    }

    function test_a_lower_bid_is_refused() {
        harness.callerIsUser("user", { sub: "alice", name: "Alice" });
        harness.subject.placeBid(40);
        harness.callerIsUser("user", { sub: "bob", name: "Bob" });
        harness.subject.placeBid(30);
        compare(harness.subject.highBid, 40);
        compare(harness.subject.highBidder, "Alice");
        compare(rejections.count, 1);
    }
}
synqt test

It needs no browser, no certificates, no database and no C++. The slots read the real Caller, created the way the web edge creates it, so a test cannot pass by stubbing the check it tests.

The other two rules are the <user> and <admin> gates in the export: block. harness.subject.placeBid(...) calls your QML function, past the gate. harness.call(...) calls the generated slot in front of it, so the gate runs first:

    function test_a_signed_out_visitor_cannot_bid() {
        harness.callerIsUser("anonymous");
        ignoreWarning(/placeBid: refused, the caller does not hold the scope it needs/);
        harness.call("placeBid", [40]);
        compare(harness.subject.highBid, 0);
    }

The books entity's rule (only the edge may call recordWinner) is its consumer list, which synqt check enforces.

A test of closeLot has a catch. closeLot records the winner through Books, and in a test the books entity is never connected (Books.ready stays false). Close a lot that has a bid and the test fails with SynQt: the 'Books' connect point is not available, so recordWinner() was not sent.

Testing your app covers the rest of EntityTest, this limit and how to work around it, and how to point schema at your schema.sql so a slot backed by Db has its tables.

Recap

You started with one live value shared across browsers and grew it, one idea at a time, into a system of three entities:

  • Connect points let entities share live, typed objects. The owner is the single authority: consumers ask, and the owner decides.
  • Sign-in gives you real identity. Authorization happens in the owner's slots, against Caller, never in the UI.
  • Entities (an edge, a database) each own their data, authenticate each other, and are segmented so the browser reaches only the edge.

The reference pages go deeper on each piece. Read the programming model next: it describes everything you just did. Before the app grows, set up testing so the rules you checked by hand stay checked. To put the app on a server, Shipping it moves this auction onto real hosts with its own pipeline and certificates, and deploying a SynQt system is the checklist to keep open while you do.