Turnout works. This part makes it something you can hand to real clubs: every change tracked, every rule tested, the obvious security holes closed, the slow parts measured, and a checklist for the day it goes live.
Do it in this order, because each step makes the next one safe:
- Source control first, so every later change can be undone.
- Tests next, so you know a change didn't break a rule.
- Security review, while the app is still small enough to read end to end.
- Performance, measured, never guessed.
- Go live, from a checklist, not from memory.
Before you start
You need Turnout as it was at the end of Part 12. If you don't have it, import the Start here app, turnout-app-part-12.zip, from the downloads at the end of this page.
Most of this part is reading and checking rather than building. The one thing you add to the app is in Lesson 13.5: phone icons. Download the five icons from this page's downloads before you get there.
Refresh the sample data. The sample events are dated from the day you last ran the seed, and the early ones are only a few days out. In SQL Workshop › SQL Scripts, run turnout_seed.sql again so they're upcoming again. It resets the sample data, including anything you changed while testing.
Lesson 13.1 The app as source files
APEX 26.2 can describe a whole application as APEXlang: plain text files, one per page and one per kind of shared component. That's how Turnout lives in Git:
turnout/
sql/ schema, seed, migrations (part5_login_codes.sql)
packages/ the business logic and its tests
apexlang/
turnout/
application.apx
pages/ p00001-discover.apx ... p09999-login.apx
shared-components/ lists, authentications, email templates, automations
workspace-components/ web credentials (no secrets)
tools/ validate and deploy scripts
You've had these files all along: every Finished download in this course is an APEXlang app in a zip. Unzip turnout-app-part-13.zip and open turnout/pages/p00020-event.apx in any text editor. You'll recognize every region, item and process you built.
Text files mean real code review: a diff shows exactly which button moved or which query changed, and a pull request can be read like any other code.
Validate before every import
SQLcl checks APEXlang without touching a database:
sql /nolog
SQL> apex validate -input turnout
SQL> exit
Install SQLcl from oracle.com/sqlcl (it needs Java 17 or later), unzip an app download, and run that from the folder that holds turnout/. "Validation successful." means the import will accept it. In a project you'd wrap the line in a small script, which is what tools/validate-apex-app.sh is in Turnout's repository.
It catches a misspelled property, a reference to a list that doesn't exist, or a missing confirmation message, in seconds. Turnout's history shows why it's worth it: the first validation found four errors that would each have failed the import.
Two lessons from building Turnout:
- The compiler version matters.
.apex/apexlang.json(a hidden folder: show hidden files to see it) names the APEXlang version, and it must match the compiler inside your SQLcl. When SQLcl is upgraded, update it. - Secrets never go in Git. Web credentials in
workspace-components/carry names only. Client IDs and secrets are typed into each workspace.
Lesson 13.2 Tests you run every time
Turnout has two test scripts, both plain PL/SQL so they run anywhere:
| Script | Checks | Expect |
|---|---|---|
turnout_api_tests.sql |
Security, capacity and waitlist, guests, membership, invitations, events | 29 PASS, all rolled back |
turnout_auth_tests.sql |
Email codes: hashing, expiry, attempts, rate limits; account linking | 13 PASS, test rows removed |
Run both after every package change, and before every import to production. A failure is a gift: it means a rule changed before a club found out.
Tests need a known starting point. The API tests expect the seed's Tuesday Track Intervals: three going, Grace then Lucas waiting. Click around that event in the app and a test fails, not because the code is wrong but because the data moved. To get back to the seed, run turnout_seed.sql again, then the tests. If your tables themselves are in a mess, run turnout_reset.sql and import the Finished app and click Install Supporting Objects: that recreates the tables, the seed and the latest packages, but it replaces your own app and its Spanish translation. In a team, give tests their own schema so nobody's clicking gets in the way.
Lesson 13.3 The security review
Read the app once, end to end, with this list:
| Check | Turnout | Where |
|---|---|---|
| Every non-public page needs sign-in | Yes: only 1, 2, 10, 20 and 9999 are public | Page › Security |
| Every organizer and admin page has a scheme | Is Organizer, Is Site Admin | Part 6 |
| Every page with ids in the URL has a checksum | Arguments Must Have Checksum, except the public pages 10 and 20 (Unrestricted on purpose, so links can be shared; everything they show is public) | Page › Security |
| Every change goes through an API that checks permissions | to_security.require_* |
Part 3 |
| Text people type is escaped when shown | apex_escape.html in messages; APEX escapes report columns by default |
Pages 130, 9999 |
| No secrets in the export | Web credentials hold names only | Part 5 |
| Codes and tokens are random enough | dbms_random today |
See below |
| APEX Advisor is clean | Run it | App Builder › Utilities › Advisor |
One upgrade before production. Sign-in codes and invite tokens use dbms_random, which is fine for a course but predictable in principle. In production, use the cryptographic generator:
l_code := lpad(mod(to_number(rawtohex(dbms_crypto.randombytes(4)), 'XXXXXXXX'), 1000000), 6, '0');
DBMS_CRYPTO needs an execute grant from your DBA, which is why the course doesn't depend on it.
Turnout's approach and Deep Data Security
APEX 26.2 can use Deep Data Security, a feature of Oracle AI Database 26ai that enforces row and column rules in the database itself. You declare data roles and data grants (for example, "a member may select the RSVPs of events in their own clubs"), and the database applies them to every query from every client. APEX passes the signed-in person's identity to the database, so a report with no where clause at all still returns only what that person may see.
| Turnout today | Deep Data Security | |
|---|---|---|
| Where the read rule lives | In every query: where r.user_id = to_security.current_user_id |
Once, in a data grant |
| Where the change rule lives | In every API procedure: to_security.require_club_role |
Still in the API: data grants govern what can be read and changed, but rules like "the waitlist moves up in order" are business logic |
| Forget a filter | That page shows too much | The database still filters it |
| Who sees the rule | Anyone reading the page or package | A DBA reading the grants; developers need to know they exist |
| Needs | Any Oracle Database and APEX version | Oracle AI Database 26ai, recent enough to include it, and configuration of end users and roles |
| Testing | Plain PL/SQL tests acting as a person (act_as) |
Tests must connect as, or impersonate, an end user |
What to choose. For Turnout as a course app, the explicit filters stay: you can read every rule, they run on apex.oracle.com and any database, and the tests prove them. For a new production app on 26ai with many reports and several ways in (APEX, REST, AI agents, reporting tools), Deep Data Security is worth the setup: one policy covers every path, and a forgotten filter stops being a data leak. Keep the API checks either way. Read rules and change rules are different jobs, and the database can't know that only an owner may hand over a club.
Lesson 13.4 Performance: measure first
With 248 people and about 120 events, every Turnout page is fast. A city with 50,000 members would find the slow parts. Find them before they do:
- Debug mode. Developer toolbar › Debug, reload, then View Debug. It lists every region with its time.
- The plan. Paste a slow query into SQL Commands and press Explain. A full scan of
TO_RSVPSis the warning sign.
Three fixes cover most of what you'll find in an app like this:
| Pattern | In Turnout | Fix when it's slow |
|---|---|---|
| A PL/SQL function called once per row | Discover calls to_rsvp_api.spots_left for every card |
Compute it in SQL with a join and group by, or keep a counter column updated by the API |
| A scalar subquery per row | My clubs counts upcoming events per club | Join to a grouped subquery once |
| A missing index on a filter column | TO_RSVPS is filtered by user_id on every personal page |
Already indexed in Part 2 (TO_RSVPS_USER_IX): check yours are too |
Measure again after each change. A fix you can't measure isn't a fix.
Lesson 13.5 Installable on phones
Turnout is now a progressive web app: on a phone, "Add to Home Screen" gives it its own icon, and it opens full screen like a native app. Two settings and five images do it:
progressiveWebApp {
enable: true
installable: true
display: standalone
themeColor: #c2410c
}
APEX finds the icons by name in Static Application Files: icons/app-icon-32.png, -192.png, -512.png, and rounded 144 and 256 versions. They're the Turnout mark from Part 9, rendered at each size. The square versions fill the whole tile, because phones round the corners themselves.
Build it.
- Shared Components › Static Application Files › Create File, Directory
icons. Upload all five icons from this page's downloads in one go:app-icon-32.png,app-icon-192.png,app-icon-512.png,app-icon-144-rounded.pngandapp-icon-256-rounded.png. Keep the names exactly as they are: APEX looks for them by name. - Shared Components › Progressive Web App. Switch Enable Progressive Web App and Installable on, Display Standalone, Screen Orientation Any, Theme Color Custom and Custom Theme Color
#c2410c. Apply Changes.
Check: run Turnout and look at the browser tab: the Turnout mark is its icon. On a phone, open the app and use Add to Home Screen; it opens full screen with the mark as its icon.
Lesson 13.6 The go-live checklist
- DEMO_MODE set to
N(Application Settings). Codes now go by email only. - MAIL_FROM set to an address your mail server may send from.
- Email sending works from your instance: send yourself a code.
- Social sign-in credentials filled in for each provider you use, and their settings set to
Y. - At least two site admins assigned in Application Access Control.
- Seed data not loaded. Create categories by hand, or with a small script.
dbms_cryptoswapped in for codes and tokens (Lesson 13.3).- Both test scripts pass against the production schema before the first real person signs in.
- APEX Advisor run, warnings read.
- Debug off, and the developer toolbar hidden from end users (it is, for anyone without a workspace login).
- A backup or export taken, and the restore tried once.
- The Spanish translation re-seeded and published after the final import (Part 12).
Checkpoint
Turnout is ready for real clubs. You can change it safely (Git and validation), prove it still works (two test scripts), say why it's secure (the review), know where it'll be slow (measured), and launch it without forgetting a step (the checklist).
| File | Change |
|---|---|
apexlang/turnout/application.apx |
Progressive web app on |
apexlang/turnout/shared-components/static-files/icons/ |
Five app icons |
apexlang/turnout/shared-components/static-files.apx |
Lists the icons |
The capstone
Build one feature on your own, end to end, using everything in the course:
Recurring events. An organizer creates "Tuesday Track Intervals, every Tuesday at 6:30 PM for 8 weeks" once, and Turnout creates the eight events.
You'll need a table change with reasons (Part 2), an API procedure with tests (Part 3), a page and a dialog (Part 7), an authorization check (Part 6), a dynamic action or two (Part 8), and Spanish text (Part 12). Ship it with the checklist in Lesson 13.6.
Downloads for Part 13
Everything this part uses. Each file is plain text you can open in any editor before you run it.
- Start hereturnout-app-part-12.zipTurnout at the end of Part 12, if you're starting at Part 13
- Finishedturnout-app-part-13.zipTurnout at the end of Part 13. Import it to compare, or to catch up
- app-icon-32app-icon-32.pngLesson 13.5: app icon, 32 pixels
- app-icon-192app-icon-192.pngLesson 13.5: app icon, 192 pixels
- app-icon-512app-icon-512.pngLesson 13.5: app icon, 512 pixels
- app-icon-144-roundedapp-icon-144-rounded.pngLesson 13.5: app icon, 144 pixels, rounded
- app-icon-256-roundedapp-icon-256-rounded.pngLesson 13.5: app icon, 256 pixels, rounded
- Part 13 kitturnout-part-13.zipEverything above in one zip, with a README
- Start overturnout_reset.sqlRemoves every TO_ object from your schema
Part 13 quiz
5 questions. Get 4 right to pass. You can retake it as often as you like.