diff --git a/CLAUDE.md b/CLAUDE.md
index c57c577..51d71bb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3,6 +3,10 @@
Minimal example/learning QQQ application (one RDBMS table + Material Dashboard).
For production scaffolding use `new-qqq-application-template` instead (per README).
+## QQQ 4.0 baseline — 2026-09-24
+
+This learning starter targets Java 21, QQQ 4.0.0 and Material Dashboard 0.41.0. Its release dependencies resolve from Maven Central; use README.md for the database, environment and build commands. The historical review below predates this migration. The separate production scaffold is outside this release train's certification.
+
## Knowledge base
Deep-review dossiers live in the second-brain vault:
diff --git a/README.md b/README.md
index 2684c8d..c47adaa 100644
--- a/README.md
+++ b/README.md
@@ -1,140 +1,56 @@
# qqq-app-starter
-[](https://opensource.org/licenses/Apache-2.0)
+A small QQQ 4.0 application for learning: one MySQL table, application navigation, and Material Dashboard 0.41.0. Requires **Java 21**, **Maven 3.8+**, and MySQL. The POM resolves QQQ 4.0.0 and the dashboard from Maven Central.
-Example QQQ application for learning and experimentation.
+## Database and configuration
-**For:** Developers learning QQQ who want a working example to explore
-**Status:** Stable
+Create a dedicated development database and user, then run this SQL in that database:
-## Why This Exists
-
-Learning a framework is easier with a running example. This starter app connects to a MySQL database and exposes a table through the QQQ dashboard. Use it to understand how QQQ metadata, entities, and the dashboard work together.
-
-For new production projects, use [new-qqq-application-template](https://github.com/QRun-IO/new-qqq-application-template) instead.
-
-## What's Included
-
-- **MetaDataProvider** - Example metadata configuration
-- **Javalin Server** - HTTP server setup
-- **Dashboard** - Material Dashboard at `/`
-- **Sample Entity** - Configurable table definition
-
-## Quick Start
-
-### Prerequisites
-
-- Java 17+
-- Maven 3.8+
-- MySQL database
-- GitHub token for Maven registry access
-
-### Maven Settings
-
-Add to `~/.m2/settings.xml`:
-
-```xml
-
-
-
- github-qqq-maven-registry
- YOUR-GITHUB-USERNAME
- YOUR-GITHUB-TOKEN
-
-
-
+```sql
+CREATE TABLE sample (
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255)
+);
+INSERT INTO sample (name) VALUES ('First example');
```
-Generate a token at: GitHub > Settings > Developer Settings > Personal access tokens > Tokens (classic) with `read:packages` scope.
-
-### Configuration
-
-Create `.env` file:
+Create a `.env` file in the project directory, using your database connection settings:
```properties
RDBMS_VENDOR=mysql
RDBMS_HOSTNAME=localhost
RDBMS_PORT=3306
-RDBMS_DATABASE_NAME=your_database
-RDBMS_USERNAME=your_user
-RDBMS_PASSWORD=your_password
+RDBMS_DATABASE_NAME=qqq_starter
+RDBMS_USERNAME=qqq_starter
+RDBMS_PASSWORD=your_local_database_password
```
-### Run
+QQQ reads this file automatically when launched from that directory. Process environment variables also work. The `.env` file is ignored by Git.
+
+## Build and run
```bash
-# Build
-mvn package
-
-# Run
+mvn clean verify
java -jar target/qqq-app-starter-0.1-SNAPSHOT.jar
```
-Open `http://localhost:8000/`
+Open , select **Sample App**, and open **Sample Table**. View the seeded row, create a row, and edit its name. The starter uses fully anonymous authentication for local learning; configure authentication before using real application data.
-## Usage
+## Customize
-### Configure Your Table
+Edit [StarterAppMetaDataProvider](src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java) to define tables, backends, branding, and navigation. Register new tables in `defineQInstance()`, point them at `rdbmsBackend`, and use `RDBMSTableBackendDetails` for the actual SQL table name. Keep the metadata fields consistent with your database schema.
-Edit `StarterAppMetaDataProvider.java`:
+[StarterAppJavalinServer](src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java) starts the HTTP server and scheduler. The bundled `StarterAppCli` is an empty placeholder, not a runnable CLI.
-```java
-private QTableMetaData defineSampleTable() {
- return new QTableMetaData()
- .withName("your_table")
- .withBackendName("rdbms")
- .withPrimaryKeyField("id")
- .withField(new QFieldMetaData("id", QFieldType.INTEGER))
- .withField(new QFieldMetaData("name", QFieldType.STRING));
-}
-```
+For migration changes, see the [QQQ 4.0 migration guide](https://github.com/QRun-IO/qqq/blob/main/docs/migration/4.0.adoc). The separate [new-qqq-application-template](https://github.com/QRun-IO/new-qqq-application-template) is a production scaffolding project outside this release train.
-### Add More Tables
+## Troubleshooting
-1. Create additional table definitions in the MetaDataProvider
-2. Register them in the `defineInstance()` method
-3. Restart the server
-
-### Running in Docker
-
-```dockerfile
-FROM openjdk:17-alpine
-EXPOSE 8000
-ADD target/qqq-app-starter-0.1-SNAPSHOT.jar app.jar
-ENTRYPOINT ["java", "-jar", "app.jar"]
-```
-
-```bash
-docker build -t qqq-app-starter .
-docker run -p 8000:8000 --env-file .env qqq-app-starter
-```
-
-## Project Structure
-
-| File | Purpose |
-|------|---------|
-| `StarterAppMetaDataProvider.java` | Defines tables, backends, apps |
-| `StarterAppJavalinServer.java` | Starts HTTP server |
-| `.env` | Database connection settings |
-
-## Common Issues
-
-**401 from Maven**: GitHub token missing or invalid. Check `~/.m2/settings.xml`.
-
-**NullPointerException in defineSampleApp**: Table name mismatch between definition and reference.
-
-**Connection refused**: Check database hostname, port, and credentials in `.env`.
-
-## Project Status
-
-Stable example project. For production apps, use [new-qqq-application-template](https://github.com/QRun-IO/new-qqq-application-template).
-
-## Contributing
-
-1. Fork the repository
-2. Create a feature branch
-3. Submit a pull request
+- **Database connection refused:** verify MySQL is listening and check the hostname/port in `.env`.
+- **Missing table:** run the SQL above in the database named by `RDBMS_DATABASE_NAME`.
+- **Missing environment variables:** launch from the directory containing `.env`, or export those variables explicitly.
+- **Wrong Java version:** both `java -version` and `mvn -version` must report Java 21 or later.
## License
-Apache-2.0 - See [LICENSE](LICENSE) for details.
+See [LICENSE](LICENSE) and [NOTICE](NOTICE).
diff --git a/pom.xml b/pom.xml
index 56feaa0..d943815 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,16 +36,16 @@
UTF-8
UTF-8
- 17
- 17
+ 21
+ 21
true
true
true
true
0.80
- 0.24.0-20250106-145452
- 0.24.0-20241122.223138-1
+ 4.0.0
+ 0.41.0
@@ -101,30 +101,13 @@
-
- org.liquibase
- liquibase-core
- 4.10.0
-
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
- 3.1.2
-
org.apache.logging.log4j
log4j-api
- 2.23.0
org.apache.logging.log4j
log4j-core
- 2.23.0
-
-
- org.apache.logging.log4j
- log4j-slf4j2-impl
- 2.23.0
org.junit.jupiter
@@ -135,7 +118,6 @@
org.assertj
assertj-core
- 3.23.1
test
@@ -222,6 +204,15 @@
3.0.0-M5
@{jaCoCoArgLine}
+ true
+
+ mysql
+ 127.0.0.1
+ 3306
+ qqq_starter_test
+ test
+ test
+
@@ -272,12 +263,7 @@
org.jacoco
jacoco-maven-plugin
- 0.8.8
-
-
- com/nutrifresh/**/*
-
-
+ 0.8.14
pre-unit-test
@@ -324,53 +310,9 @@
exec-maven-plugin
org.codehaus.mojo
3.0.0
-
-
- test-coverage-summary
- verify
-
- exec
-
-
- sh
-
- -c
-
- /tmp/$$.headers
-xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
-echo
-echo "Jacoco coverage summary report:"
-echo " See also target/site/jacoco/index.html"
-echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
-echo "------------------------------------------------------------"
-paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
-rm /tmp/$$.headers /tmp/$$.values
- ]]>
-
-
-
-
-
-
-
- github-qqq-maven-registry
- GitHub QQQ Maven Registry
- https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
-
-
-
-
-
- github-qqq-maven-registry
- GitHub QQQ Maven Registry
- https://maven.pkg.github.com/Kingsrook/qqq-maven-registry
-
-
-
diff --git a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java
index ea1d728..43d528d 100644
--- a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java
+++ b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppJavalinServer.java
@@ -23,26 +23,23 @@ package com.kingsrook.qqq.starterapp;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
/*******************************************************************************
- ** Start a javalin (http) qqq server.
- **
- ** Supported system properties:
- **
- ** -Dqqq.scheduleManager.enabled=false - do not start the ScheduleManager
- ** (used inside that class).
- **
- ** -Dqqq.javalin.hotSwapInstance=true - to cause the QInstance to be hot-swapped.
- ** Useful during development, to avoid needing as many server restarts.
+ ** Starts the starter application's HTTP server and scheduled jobs.
*******************************************************************************/
-public class StarterAppJavalinServer
+public class StarterAppJavalinServer extends QApplicationJavalinServer
{
- private static final QLogger LOG = QLogger.getLogger(StarterAppJavalinServer.class);
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public StarterAppJavalinServer()
+ {
+ super(new StarterAppMetaDataProvider());
+ }
@@ -51,29 +48,8 @@ public class StarterAppJavalinServer
*******************************************************************************/
public static void main(String[] args) throws QException
{
- try
- {
- /////////////////////////////
- // define your application //
- /////////////////////////////
- StarterAppMetaDataProvider application = new StarterAppMetaDataProvider();
-
- /////////////////////////////////////
- // start the javalin (http) server //
- /////////////////////////////////////
- QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(application);
- javalinServer.start();
-
- //////////////////////////////////////////////////////////////////////////
- // if you want to use scheduled jobs, start the schedule manager module //
- //////////////////////////////////////////////////////////////////////////
- QScheduleManager scheduleManager = QScheduleManager.initInstance(application.defineQInstance(), () -> new QSystemUserSession());
- scheduleManager.start();
- }
- catch(Exception e)
- {
- LOG.error("Error starting application server.", e);
- }
+ new StarterAppJavalinServer().start();
+ QScheduleManager scheduleManager = QScheduleManager.initInstance(new StarterAppMetaDataProvider().defineQInstance(), () -> new QSystemUserSession());
+ scheduleManager.start();
}
-
}
diff --git a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java
index 16382df..bd1e89c 100644
--- a/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java
+++ b/src/main/java/com/kingsrook/qqq/starterapp/StarterAppMetaDataProvider.java
@@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.AuthScope;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@@ -64,7 +65,7 @@ public class StarterAppMetaDataProvider extends AbstractQQQApplication
QInstance qInstance = new QInstance();
qInstance.setBranding(defineBranding());
- qInstance.setAuthentication(defineAuthentication());
+ qInstance.registerAuthenticationProvider(AuthScope.instanceDefault(), defineAuthentication());
qInstance.addBackend(defineRDBMSBackend());
qInstance.addTable(defineSampleTable());
qInstance.addApp(defineSampleApp(qInstance));
diff --git a/src/test/java/com/kingsrook/qqq/starterapp/StarterAppTest.java b/src/test/java/com/kingsrook/qqq/starterapp/StarterAppTest.java
new file mode 100644
index 0000000..e5d5ab5
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/starterapp/StarterAppTest.java
@@ -0,0 +1,85 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.starterapp;
+
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicReference;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import io.javalin.Javalin;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Verifies the new-user HTTP entry point and its registered table metadata.
+ *******************************************************************************/
+class StarterAppTest
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testServer_servesDashboardAndSampleTable() throws Exception
+ {
+ StarterAppJavalinServer server = new StarterAppJavalinServer();
+ AtomicReference service = new AtomicReference<>();
+ server.setPort(0);
+ server.withJavalinConfigurationCustomizer(service::set);
+ server.withJavalinConfigCustomizer(config -> config.routes.get("/starter-configuration-check",
+ context -> context.status(202).result("configured")));
+
+ try
+ {
+ server.start();
+ URI baseUri = URI.create("http://localhost:" + service.get().port());
+ try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build())
+ {
+ HttpResponse dashboard = client.send(HttpRequest.newBuilder(baseUri.resolve("/")).timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString());
+ assertEquals(200, dashboard.statusCode());
+ assertTrue(dashboard.body().contains(" metadata = client.send(HttpRequest.newBuilder(baseUri.resolve("/metaData")).timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString());
+ assertEquals(200, metadata.statusCode());
+ JSONObject metadataBody = new JSONObject(metadata.body());
+ assertTrue(metadataBody.getJSONObject("tables").has("sampleTable"));
+ assertTrue(metadataBody.getJSONObject("apps").has("sampleApp"));
+
+ HttpResponse configured = client.send(HttpRequest.newBuilder(baseUri.resolve("/starter-configuration-check"))
+ .timeout(Duration.ofSeconds(5)).build(), HttpResponse.BodyHandlers.ofString());
+ assertEquals(202, configured.statusCode());
+ assertEquals("configured", configured.body());
+ }
+ }
+ finally
+ {
+ server.stop();
+ QContext.clear();
+ }
+ }
+}