Skip to content

NOMVEX Apps

Your app sketched with you, priced before we start, yours forever.

Request → Review & plan → Build → Delivery12341. Request2. Review &plan3. Build4. Delivery
NOMVEX APPS

Estimate my app

Write what you need in a few sentences. We find the features already built and tested, and the part left to build. The technical detail stays available for developers.

1. Your request

Who uses the app?
Features are recognised by keywords, with no AI: the same request always gives the same plan.

2. Estimate: 25% already built and tested

Reused (tested engines)

  • Compliant invoices (VAT, legal notes)

New work

  • Slot booking : Slots, capacity, book and cancel with no double booking.
  • Accounts and sign-in : Roles and access rights per screen.
  • Notifications and reminders : When to notify whom, exactly once.

Found in your text: Compliant invoices (VAT, legal notes) (« invoic ») · Slot booking (« booking, slot ») · Accounts and sign-in (« accounts ») · Notifications and reminders (« sms, reminders »)

Keyword estimate, capped at 90%; NOMVEX confirms the real figure at review. It is not a price discount.

Screen sketch

sketchYour nameNext eventMain actionNewsHomesketchSign-upFirst nameCategoryConsentSendSign-upsketchThis weekSlotsBookSchedulesketchPaymentAmount you setPayPaidPaymentsketchConversationsMessagesWrite…Messages
Automatic sketch, no AI, from your description: it is not the final design.

Shipped with every app

  • 3 test files, including 74 access checks43 refusals checked, 3 “own records only” cases (a customer sees only their own orders, a parent only their own child).
  • Full export of your dataEvery table as JSON and CSV, at any time. You get the code: you can host it elsewhere.
  • Installs on the home screen, no app storePages already opened stay readable offline (basement, market, building site). On iPhone, notifications need iOS 16.4 or later and the app added to the home screen.
  • Activity summary (grant, annual report)Totals only, no names; counts from 1 to 4 are hidden.

Figures computed from the test files generated for this app (see the technical detail), not a general promise.

Technical detail (for developers)
  • Compliant invoices (VAT, legal notes) · lib/business/invoice.ts · tests scripts/test-invoice.ts

3. Files to create (12)

lib/apps/built/club-app/index.ts
// club-app: generated by lib/apps/compile.ts. Reused engines are imported as they are; new work lives next to this file.
import * as invoices from '../../../../lib/business/invoice';
import * as booking from './booking';
import * as accounts from './accounts';
import * as notifications from './notifications';

export const engines={invoices,booking,accounts,notifications};
/** Server engines, called through their existing network-service routes. */
export const serverEngines:string[]=[];
export const reusePct=25;
lib/apps/built/club-app/booking.ts
/** Slot booking / Réservation de créneaux
 * To build: Slots, capacity, book and cancel with no double booking.
 * À faire : Créneaux, capacité, réservation et annulation sans double réservation. */
export type BookingInput=Readonly<Record<string,unknown>>;
export type BookingResult={ok:boolean;errors:string[]};

export function booking(input:BookingInput):BookingResult{
  void input;
  throw new Error('not_implemented:booking');
}
lib/apps/built/club-app/accounts.ts
/** Accounts and sign-in / Comptes et connexion
 * To build: Roles and access rights per screen.
 * À faire : Rôles et droits d’accès par écran. */
export type AccountsInput=Readonly<Record<string,unknown>>;
export type AccountsResult={ok:boolean;errors:string[]};

export function accounts(input:AccountsInput):AccountsResult{
  void input;
  throw new Error('not_implemented:accounts');
}
lib/apps/built/club-app/notifications.ts
/** Notifications and reminders / Notifications et rappels
 * To build: When to notify whom, exactly once.
 * À faire : Quand prévenir qui, et une seule fois. */
export type NotificationsInput=Readonly<Record<string,unknown>>;
export type NotificationsResult={ok:boolean;errors:string[]};

export function notifications(input:NotificationsInput):NotificationsResult{
  void input;
  throw new Error('not_implemented:notifications');
}
scripts/test-club-app.ts
import assert from 'node:assert/strict';
import {engines,reusePct} from '../lib/apps/built/club-app/index';

// Reused engines: the functions this app calls exist (their own tests cover the behaviour).
assert.equal(typeof engines.invoices.totals,'function','lib/business/invoice.ts totals');
assert.equal(typeof engines.invoices.validateInvoice,'function','lib/business/invoice.ts validateInvoice');
assert.equal(typeof engines.invoices.legalMentions,'function','lib/business/invoice.ts legalMentions');
assert.equal(reusePct,25);

// New work: each stub fails until it is built. Replace these with real cases.
assert.throws(()=>engines.booking.booking({}),/not_implemented:booking/);
assert.throws(()=>engines.accounts.accounts({}),/not_implemented:accounts/);
assert.throws(()=>engines.notifications.notifications({}),/not_implemented:notifications/);

console.log('test-club-app.ts: ok');
app/apps/built/club-app/page.tsx
export const metadata={title:'club-app | NOMVEX Apps'};

export default function Page(){
  return <main className="shell"><div className="pagehead"><h1>club-app</h1></div>
    <section className="card"><h2>Compliant invoices (VAT, legal notes)</h2></section>
    <section className="card"><h2>Slot booking</h2></section>
    <section className="card"><h2>Accounts and sign-in</h2></section>
    <section className="card"><h2>Notifications and reminders</h2></section>
  </main>;
}
lib/apps/built/club-app/access.ts
// club-app: access policy generated by lib/apps/delivery-kit.ts. Deny by default; edit the rules, then re-run the access test.
import {can,type Matrix} from '../../role-matrix';

export const MATRIX:Matrix={"roles":["member","board","public"],"resources":["public-info","profile","roster","schedule","payment-status","admin"],"rules":[{"role":"public","resource":"public-info","action":"read","scope":"all"},{"role":"member","resource":"public-info","action":"read","scope":"all"},{"role":"board","resource":"public-info","action":"read","scope":"all"},{"role":"member","resource":"profile","action":"read","scope":"own"},{"role":"member","resource":"profile","action":"write","scope":"own"},{"role":"member","resource":"schedule","action":"read","scope":"all"},{"role":"member","resource":"payment-status","action":"read","scope":"own"},{"role":"board","resource":"profile","action":"read","scope":"all"},{"role":"board","resource":"profile","action":"write","scope":"all"},{"role":"board","resource":"roster","action":"read","scope":"all"},{"role":"board","resource":"roster","action":"write","scope":"all"},{"role":"board","resource":"schedule","action":"read","scope":"all"},{"role":"board","resource":"schedule","action":"write","scope":"all"},{"role":"board","resource":"payment-status","action":"read","scope":"all"},{"role":"board","resource":"payment-status","action":"write","scope":"all"},{"role":"board","resource":"admin","action":"read","scope":"all"},{"role":"board","resource":"admin","action":"write","scope":"all"}]};
export const allowed=(role:string,action:'read'|'write',resource:string,own:boolean)=>can(MATRIX,role,action,resource,own);
scripts/test-club-app-access.ts
import assert from 'node:assert/strict';
import {allowed} from '../lib/apps/built/club-app/access';

// One line per role, resource, action and record owner. "own=false" is someone else's record.
assert.equal(allowed('member','read','public-info',true),true,'member read public-info own=true');
assert.equal(allowed('member','read','public-info',false),true,'member read public-info own=false');
assert.equal(allowed('member','write','public-info',true),false,'member write public-info own=true');
assert.equal(allowed('member','write','public-info',false),false,'member write public-info own=false');
assert.equal(allowed('member','read','profile',true),true,'member read profile own=true');
assert.equal(allowed('member','read','profile',false),false,'member read profile own=false');
assert.equal(allowed('member','write','profile',true),true,'member write profile own=true');
assert.equal(allowed('member','write','profile',false),false,'member write profile own=false');
assert.equal(allowed('member','read','roster',true),false,'member read roster own=true');
assert.equal(allowed('member','read','roster',false),false,'member read roster own=false');
assert.equal(allowed('member','write','roster',true),false,'member write roster own=true');
assert.equal(allowed('member','write','roster',false),false,'member write roster own=false');
assert.equal(allowed('member','read','schedule',true),true,'member read schedule own=true');
assert.equal(allowed('member','read','schedule',false),true,'member read schedule own=false');
assert.equal(allowed('member','write','schedule',true),false,'member write schedule own=true');
assert.equal(allowed('member','write','schedule',false),false,'member write schedule own=false');
assert.equal(allowed('member','read','payment-status',true),true,'member read payment-status own=true');
assert.equal(allowed('member','read','payment-status',false),false,'member read payment-status own=false');
assert.equal(allowed('member','write','payment-status',true),false,'member write payment-status own=true');
assert.equal(allowed('member','write','payment-status',false),false,'member write payment-status own=false');
assert.equal(allowed('member','read','admin',true),false,'member read admin own=true');
assert.equal(allowed('member','read','admin',false),false,'member read admin own=false');
assert.equal(allowed('member','write','admin',true),false,'member write admin own=true');
assert.equal(allowed('member','write','admin',false),false,'member write admin own=false');
assert.equal(allowed('board','read','public-info',true),true,'board read public-info own=true');
assert.equal(allowed('board','read','public-info',false),true,'board read public-info own=false');
assert.equal(allowed('board','write','public-info',true),false,'board write public-info own=true');
assert.equal(allowed('board','write','public-info',false),false,'board write public-info own=false');
assert.equal(allowed('board','read','profile',true),true,'board read profile own=true');
assert.equal(allowed('board','read','profile',false),true,'board read profile own=false');
assert.equal(allowed('board','write','profile',true),true,'board write profile own=true');
assert.equal(allowed('board','write','profile',false),true,'board write profile own=false');
assert.equal(allowed('board','read','roster',true),true,'board read roster own=true');
assert.equal(allowed('board','read','roster',false),true,'board read roster own=false');
assert.equal(allowed('board','write','roster',true),true,'board write roster own=true');
assert.equal(allowed('board','write','roster',false),true,'board write roster own=false');
assert.equal(allowed('board','read','schedule',true),true,'board read schedule own=true');
assert.equal(allowed('board','read','schedule',false),true,'board read schedule own=false');
assert.equal(allowed('board','write','schedule',true),true,'board write schedule own=true');
assert.equal(allowed('board','write','schedule',false),true,'board write schedule own=false');
assert.equal(allowed('board','read','payment-status',true),true,'board read payment-status own=true');
assert.equal(allowed('board','read','payment-status',false),true,'board read payment-status own=false');
assert.equal(allowed('board','write','payment-status',true),true,'board write payment-status own=true');
assert.equal(allowed('board','write','payment-status',false),true,'board write payment-status own=false');
assert.equal(allowed('board','read','admin',true),true,'board read admin own=true');
assert.equal(allowed('board','read','admin',false),true,'board read admin own=false');
assert.equal(allowed('board','write','admin',true),true,'board write admin own=true');
assert.equal(allowed('board','write','admin',false),true,'board write admin own=false');
assert.equal(allowed('public','read','public-info',true),true,'public read public-info own=true');
assert.equal(allowed('public','read','public-info',false),true,'public read public-info own=false');
assert.equal(allowed('public','write','public-info',true),false,'public write public-info own=true');
assert.equal(allowed('public','write','public-info',false),false,'public write public-info own=false');
assert.equal(allowed('public','read','profile',true),false,'public read profile own=true');
assert.equal(allowed('public','read','profile',false),false,'public read profile own=false');
assert.equal(allowed('public','write','profile',true),false,'public write profile own=true');
assert.equal(allowed('public','write','profile',false),false,'public write profile own=false');
assert.equal(allowed('public','read','roster',true),false,'public read roster own=true');
assert.equal(allowed('public','read','roster',false),false,'public read roster own=false');
assert.equal(allowed('public','write','roster',true),false,'public write roster own=true');
assert.equal(allowed('public','write','roster',false),false,'public write roster own=false');
assert.equal(allowed('public','read','schedule',true),false,'public read schedule own=true');
assert.equal(allowed('public','read','schedule',false),false,'public read schedule own=false');
assert.equal(allowed('public','write','schedule',true),false,'public write schedule own=true');
assert.equal(allowed('public','write','schedule',false),false,'public write schedule own=false');
assert.equal(allowed('public','read','payment-status',true),false,'public read payment-status own=true');
assert.equal(allowed('public','read','payment-status',false),false,'public read payment-status own=false');
assert.equal(allowed('public','write','payment-status',true),false,'public write payment-status own=true');
assert.equal(allowed('public','write','payment-status',false),false,'public write payment-status own=false');
assert.equal(allowed('public','read','admin',true),false,'public read admin own=true');
assert.equal(allowed('public','read','admin',false),false,'public read admin own=false');
assert.equal(allowed('public','write','admin',true),false,'public write admin own=true');
assert.equal(allowed('public','write','admin',false),false,'public write admin own=false');
// Unknown roles and resources are always refused.
assert.equal(allowed('intruder','read','profile',true),false);
assert.equal(allowed('board','read','not-a-resource',true),false);

console.log('test-club-app-access.ts: ok (74 access checks)');
lib/apps/built/club-app/export.ts
// club-app: full data export. The user owns their data: every table, as JSON and as CSV, at any time.
import {exportCsv,type Entry} from '../../templates';

export function exportAll(tables:Readonly<Record<string,ReadonlyArray<Entry>>>){
  const csv:Record<string,string>={};
  for(const [name,rows] of Object.entries(tables))csv[name]=exportCsv(rows);
  return {json:JSON.stringify({app:'club-app',exportedAt:new Date().toISOString(),tables},null,2),csv};
}
lib/apps/built/club-app/impact.ts
// club-app: activity summary for grant dossiers and reports. Aggregates only; counts from 1 to 4 are shown as "< 5".
export {impactCells,impactCsv,MIN_CELL} from '../../impact';
public/apps/club-app/manifest.webmanifest
{
  "name": "club-app",
  "short_name": "club-app",
  "start_url": "/apps/built/club-app",
  "scope": "/apps/built/club-app",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#ffffff",
  "lang": "fr",
  "icons": []
}
public/apps/club-app/sw.js
// club-app: offline service worker. Same-origin GET only; pages fall back to the last copy when there is no network.
const CACHE='club-app-v1';
self.addEventListener('install',e=>{e.waitUntil(caches.open(CACHE).then(c=>c.add('/apps/built/club-app')));self.skipWaiting();});
self.addEventListener('activate',e=>{e.waitUntil(caches.keys().then(k=>Promise.all(k.filter(x=>x!==CACHE).map(x=>caches.delete(x)))));});
self.addEventListener('fetch',e=>{
  const r=e.request;if(r.method!=='GET'||new URL(r.url).origin!==self.location.origin)return;
  e.respondWith(fetch(r).then(res=>{if(res.ok&&res.type==='basic'){const copy=res.clone();caches.open(CACHE).then(c=>c.put(r,copy));}return res;}).catch(()=>caches.match(r).then(m=>m||caches.match('/apps/built/club-app'))));
});

Tests to run

S=$(mktemp -d); npx tsc --target ES2022 --module commonjs --moduleResolution node --strict --skipLibCheck --esModuleInterop --outDir $S scripts/test-invoice.ts && node $S/scripts/test-invoice.js
S=$(mktemp -d); npx tsc --target ES2022 --module commonjs --moduleResolution node --strict --skipLibCheck --esModuleInterop --outDir $S scripts/test-club-app.ts && node $S/scripts/test-club-app.js
S=$(mktemp -d); npx tsc --target ES2022 --module commonjs --moduleResolution node --strict --skipLibCheck --esModuleInterop --outDir $S scripts/test-club-app-access.ts && node $S/scripts/test-club-app-access.js

Cost over 3 years for your business

Add the offers you are comparing, with the price read on their page, the page address and the date you read it. NOMVEX supplies no competitor price: a row with no source, or read more than 90 days ago, is not drawn. Rows stay in this browser.

No offer drawn yet.

NOMVEX: price after review (rate grid not published yet).

Comparison built from the prices you typed, on the dates shown, for the same use. Check each page before sharing it.

Activity summary (grant, annual report, bank)

Totals only, never names. A count from 1 to 4 shows as “< 5” so nobody can be recognised, a minor above all. Nothing is sent.

Summary
IndicatorPeriod 2026-2027
Customers or members—
of whom under 18—
Staff or volunteers—
Appointments or bookings—
Events—

From request to app | NOMVEX Apps | NOMVEX Apps