August 14, 2026

From Dancexam to DanceTech

Check-in, Stripe payments, and YAML exams for a weekly West Coast Swing studio

rustaxumhtmxaskamastripepostgresredisdanceweb development
View on GitHub →

DanceTech is the website a weekly dance community uses to run the door, take payment, and administer advancement exams. It is live for the Greenville Westies. The source is on GitHub.

If you have never been to a West Coast Swing night, the rest of this will not make sense without a little context. I will start there, then walk through what the app actually does, why I built it in Rust, and the design choices that only make sense once you have seen a real studio try to run a night.

DanceTech Logo

The problem a weekly studio actually has

West Coast Swing is a partner dance. Communities like the Greenville Westies meet every week for a lesson and a social. Lessons are leveled — beginner, then something like beginner-plus or intermediate, then advanced — and the two roles in the partnership are leader and follower. You do not just walk into the advanced class because you feel ready. Most scenes use a progression test: an instructor watches you dance a set of patterns and technique, scores you, and decides whether you can move up.

That test is only one piece of the night. Someone at the door still has to answer:

  • Who is here?
  • Have they paid for the lesson, the social, or both?
  • Are they allowed in the class they just asked for?

Before DanceTech, those answers lived in a mix of cash, memory, and spreadsheets. I had already built Dancexam in 2024 to grade the tests. It solved the scoring problem and left the rest of the night on paper. The people taking the test, the people paying at the door, and the people allowed into advanced class are the same people. Running two systems for that was going to mean two logins, two user lists, and two things to deploy every week.

DanceTech is Dancexam grown into the operations layer for that night: accounts, check-in, payments, the exam queue, and the same tests.

Greenville is the first deployment, not a hard-coded one-off. Another studio should be able to reuse it by writing their own exam files and tagging their own products in Stripe.

What a night looks like

A dancer creates an account — email and password, or Google — and shows up to check in. The check-in page is a catalog of what is for sale tonight: beginner lesson, social dance, an advanced class, whatever the studio put in Stripe. They only see products they are allowed to buy. If they have not passed the leader test, the advanced leader class is hidden, or shown greyed out as a preview so they know it exists. Some products only appear during a time window, so the Thursday class does not sit on the site all week inviting people to pay for something that is not happening.

They add items to a cart and pay. The card never hits this server. DanceTech asks Stripe — a payments company — to open a hosted Checkout page, Stripe collects the money, and the dancer comes back to a receipt the door can trust. Old receipt links expire so someone cannot screenshot last month’s success page and walk in free.

Later in the night, someone wants to test up. They join a live queue for the leader or follower exam. A proctor — an instructor allowed to administer tests — pulls them from the queue, scores them while they dance, and submits the form. Live grading means the running score updates as boxes get checked, so the proctor can see whether the dancer is already above or below the 60% pass line before the last pattern. A pass writes a role onto the account, advanced-leader or advanced-follower. The next week, check-in shows them the class that role unlocks.

Admins can edit roles by hand, refresh the product list from Stripe, and export CSVs of users, exams, and who has which role. That last part matters when the studio wants a roster that is not trapped in the app.

flowchart TD
    A[Log in] --> B["Check-in shows only the classes<br/>you qualify for"]
    B --> C[Pay on Stripe]
    C --> D["Join the leader or follower<br/>exam queue"]
    D --> E[Proctor scores while you dance]
    E --> F["A pass grants advanced-leader<br/>or advanced-follower roles"]
    F -->|next week| B

The loop is the useful part. The exam is not a trophy sitting in a different database. Passing changes what you can buy next week.

Why I wrote it in Rust

The honest answer is not “type safety” or “performance.” I was at GE, and there was a team I wanted to join that wrote Rust. I needed a real project to learn the language, not another tutorial. After the Dance DJ Assistant in Python and Flask, Dancexam was that project: a production web app with a database, auth, templates, and a deploy story, written in a language I did not know yet.

The 2024 write-up is about that learning curve — ownership, borrowing, getting something onto a server. This post is about what the app became once a community used it every week. I stayed on Rust because the exam tool already worked, and because rewriting a live studio app in a different language to prove a point would have been a good way to have no app on Thursday.

The stack, in plain language

The server is Rust, using Axum as the web framework (the same job Flask or Express does). Pages are HTML templates rendered by Askama, which checks the templates when the program compiles. Interactivity is HTMX: instead of writing a separate JavaScript frontend, buttons and forms ask the server for a snippet of HTML and swap it into the page. For a form-heavy studio app — carts, queues, scoring sheets — that is enough.

Data that has to survive a restart lives in PostgreSQL. Short-lived things — login sessions and shopping carts — live in Redis. Payments go through Stripe Checkout. The look of the site is Tailwind and Flowbite, downloaded into the repo rather than pulled from a public CDN at runtime, so a third-party JavaScript host cannot take the door down.

Why the design looks like this

One app, because it is one night

Check-in and exams share users and roles. Splitting them would have meant two auth systems and two deploy stories for the same people walking through the same door. Folding payments into the exam app was less “add a feature” and more “stop pretending these are different products.”

Stripe owns the card, DanceTech owns who can buy

I did not want credit card numbers on a server I run for a dance community. PCI rules around storing cards are a full-time job I do not want. Hosted Checkout is less custom UI and far less liability: Stripe’s page takes the payment, DanceTech creates the session and shows a receipt.

What is for sale is also Stripe’s problem, on purpose. Products in the Stripe Dashboard get metadata tags:

  • show-on-dancetech — this product appears in the app at all
  • requires-roles — the buyer must already have roles like advanced-leader
  • show-weekly / show-interval — only sell it during a window, e.g. Thursday 18:00–23:00
  • show-preview — let people without the role see a greyed-out teaser
  • sort-level — control the order on the page

A background task refreshes that catalog every few minutes, and an admin can force a refresh, so a check-in request does not wait on Stripe’s API while a line forms at the door. Changing the price of the social, or hiding a workshop after the weekend, does not require a code deploy. The studio manager edits Stripe. The app notices.

Exams are text files, not Rust

The leader and follower tests live as YAML under test_definitions/. YAML is a plain-text format that looks like a structured list. Instructors can change a pattern, a point value, or the pass threshold without asking me to ship a new binary. Passing the standard leader test grants advanced-leader; the follower test grants advanced-follower. Those strings are not compiled into the app. They are data, and Stripe products can require the same strings.

That is the same idea as the catalog. The people who know what a good whip looks like should be able to edit the scoring sheet. The people who know what Thursday costs should be able to edit the product. Neither of those people should have to learn Rust.

Login is not permission

Anyone in the community can have an account. That does not mean they can grade a test or edit someone else’s roles. Routes are grouped into “must be logged in,” “check if they are logged in,” and “no account needed.” The interesting checks sit next to the action: administering an exam, searching the roster, or pulling someone else out of the queue requires Admin or Proctor.

Roles that come from tests are different. Admin and Proctor are built in. advanced-leader is not. The app stores whatever string the exam file says to grant, and check-in compares that string to the Stripe tag. A new level of class does not need a code change. It needs a test that grants a role and a product that requires it.

One place for names the UI and the server both use

This is the most “programmer” part of the design, and it exists because of HTMX.

HTMX works by pointing at HTML ids: “when this button is clicked, replace the div called queue-container.” Those names appear in Rust handlers and in HTML templates. If they drift — queue-container in one file and queue_container in the other — the button silently does nothing. There is no compiler for a typo in a string.

So every URL lives on one ROUTES struct in src/app/router.rs. The Axum router, redirects, and templates all read rts.login or ROUTES.check_in. Parameterized paths are methods on the same type, so renaming a route cannot drift between Rust and HTML.

HTML ids follow the same idea. Each feature has an Ids struct:

pub struct Ids {
    pub test_form: &'static str,
    pub queue_container: &'static str,
    pub refresh_queue_event: &'static str,
    pub queue_reload_handler: &'static str,
}

pub const IDS: Ids = Ids {
    test_form: "search-tests-form",
    queue_container: "queue-container",
    refresh_queue_event: "refresh-queue",
    queue_reload_handler: "queue-reload-handler",
};

Handlers and templates use those fields. A typo in hx-target or HX-Trigger fails when the app compiles instead of as a silent no-op on Thursday night. That is the cheapest way I have found to keep a server-rendered UI honest.

What is still unfinished

Demo mode is incomplete, so I leave it off. Emailing exam results on completion is not built — you can see that leftover in the code — and the queue is missing a few niceties. The useful part is already in weekly use, which is the part I care about.

If you want the original learning-Rust narrative, start with From Python to Rust — Building a Production Web Application. The running system is at greenvillewesties.com.