Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: keynote-demo-app description: Build or extend a SQLCon EU 2026 keynote demo application so it looks and behaves like the other keynote apps. Use when creating a new demo view (invoices, operations, orders), restyling an existing app to match, adding a deliberate database anti-pattern for a troubleshooting demo, or wiring an app to Azure SQL or Fabric SQL with managed identity. Triggers include "make this look like the keynote app", "build a demo view", "add the slow query demo", "match Caldova styling".
Keynote demo app
Two keynote demos share one product identity. Demo 2 shows evidence search on Azure SQL Hyperscale. Demo 3 shows a different view of the same product running slowly, then fixed. They must read as one application. This skill is how you keep that true.
Non-negotiables
- Import the shared tokens. Never invent colours or fonts.
``css @import '../../shared/design-system/tokens.css'; ` Palette: canvas #f8f9f6, ink #183129, forest #1b5343, coral #e27156`. Type: Manrope for UI, Newsreader for headings, DM Mono for anything numeric or SQL.
- Reuse the layout skeleton. Sticky
.topbarwith.brand+.brand-mark, a centred
.workspace, an .eyebrow above the h1, and a .metric-band for live numbers.
- Show the SQL. Every keynote app has a tab or panel that displays the exact
parameterized statement it just ran. The audience needs to see it is real SQL.
- Never fake a number. If the database is unreachable or a contract check fails, render
-- and say so. Do not interpolate, estimate, or reuse a previous value. This rule cost real work in demo 2 and it is the reason the demo is trustworthy.
- Parameterize everything. No string-concatenated SQL, ever. It is both a security
issue and the subject of demo 3.
Building a new view
Start from demo3-estate-running-itself/app. It is deliberately small: a Vite React client, an Express API, and one SQL module. Copy it, rename the entity, keep the shell.
Structure to preserve:
app/server/index.ts Express API. One route per screen. Parameterized queries only.src/App.tsx One page. Search control, metric band, results table, SQL panel.src/App.css View-specific styles only. Tokens come from shared/.
Connecting to the database
Use Microsoft Entra authentication and a managed identity. Never put a SQL login in code or in an environment file.
import { DefaultAzureCredential } from '@azure/identity'import sql from 'mssql' // NOTE: default import. mssql is CommonJS.import type * as MSSQL from 'mssql' // types onlyconst token = await new DefaultAzureCredential().getToken('https://database.windows.net/.default')const pool = await new sql.ConnectionPool({server: process.env.AZURE_SQL_SERVER,database: process.env.AZURE_SQL_DATABASE,authentication: { type: 'azure-active-directory-access-token', options: { token: token.token } },options: { encrypt: true, trustServerCertificate: false },}).connect()
Grant the identity a least-privilege role, not db_owner:
CREATE USER [my-app-identity] FROM EXTERNAL PROVIDER;ALTER ROLE MyAppReader ADD MEMBER [my-app-identity];
Known traps
These cost hours during demo 2. Do not rediscover them.
import * as sql from 'mssql'compiles but throwssql.ConnectionPool is not a constructor
at runtime. mssql is CommonJS with no ESM named exports. Use a default import.
COUNT_BIGcomes back as a string through tedious.count === 1000is always false.
Wrap in Number().
CAST(vector AS NVARCHAR(MAX))is lossy at about 8 significant digits, so it cannot verify a
float32 round trip. Compare vectors in-engine with VECTOR_DISTANCE('euclidean', a, b) = 0.
- Docker
COPYpreserves restrictive host file permissions. If files come from OneDrive, add
COPY --chown=node:node and chmod, or the container fails with EACCES.
- A Container Apps job with a system-assigned identity cannot pull its own first image. Create a
user-assigned identity, grant AcrPull, then reference it with --registry-identity.
Adding a deliberate anti-pattern
Demo 3 needs a query that is slow for a reason a human can explain in one sentence. The approved one is a parameter type mismatch that forces a scan instead of a seek.
Make the column and the parameter disagree:
CREATE TABLE dbo.Invoice (InvoiceNumber VARCHAR(20) NOT NULL, ...);CREATE NONCLUSTERED INDEX IX_Invoice_InvoiceNumber ON dbo.Invoice (InvoiceNumber);
// Slow: NVarChar parameter against a VARCHAR column forces CONVERT_IMPLICIT on every row.request.input('invoiceNumber', sql.NVarChar(20), value)// Fixed: matching type allows an index seek.request.input('invoiceNumber', sql.VarChar(20), value)
The fix is one word in the application. That is the point of the demo: the database was never wrong, and nobody had to change the schema.
Keep both paths in the code behind a config flag so the before and after can be shown live without editing files on stage.
Writing copy for the app
Match the house voice: short sentences, plain English, direct address. Say "1,000 chunks" not "a corpus of approximately one thousand document fragments". Avoid "leverage", "seamless", "powerful", and "exciting".