Portfolio·Bespoke lane

English
with Flor

A full-stack bilingual website for a Buenos Aires language school. Flask web app with student registration, course enrolment, a user dashboard, and an admin panel — paired with a FastAPI REST API sharing the same data layer.

The brief

Flor runs a small English school serving Argentine students at CEFR levels A1 through B2. She needed a proper web presence — not a page builder template — that reflected the school's conversational, student-first approach and handled the operational side: course information, enrolment, and direct contact.

Argentine students bring specific complexity: two student types (domestic and international) with different identity document requirements, and a primarily Spanish-speaking audience that the site needed to serve natively alongside English.

Approach

Built as a Flask application with a Jinja2 template layer — full server-side rendering, no JavaScript framework. Bilingual support implemented as a translation dictionary keyed on a ?lang= URL parameter, applied consistently across every page, form label, and validation message.

A companion FastAPI service sits alongside the Flask app, sharing the same SQLite database. It exposes a documented REST API for external integrations — JWT-authenticated, rate-limited, and fully tested against an in-memory database.

Homepage

Hero section with rotating location imagery, a "Now enrolling for 2026" badge, bilingual EN/ES toggle, and clear CTAs into the course catalogue and registration flow.

Homepage hero. EN/ES toggle in the nav applies the full translation dictionary site-wide via ?lang= URL parameter.
Method

Three-step learning method presented with a numbered layout — Immerse & Listen, Practice & Speak, Refine & Master — alongside an "Our Method" editorial section explaining the conversation-first approach.

Method section and a student testimonial in Spanish — the site's primary language for the target audience.
Social proof + CTA

Student testimonials in the students' own language (Spanish), followed by a high-contrast "Book your level test" call-to-action — the primary conversion event for prospective students.

Testimonials and level test CTA. The dark section creates visual contrast before the footer.
Course catalogue

Four active CEFR levels (A1–B2) presented as cards. C1 and C2 are built but hidden behind a feature flag for staged release — no code changes needed to enable them.

Course catalogue. C1 and C2 exist in the codebase but are gated by a boolean feature flag in routes.py.
Course detail

Each level has its own page with curriculum outcomes, format and schedule details, pricing, and a direct enrolment CTA. Content is structured for the student to self-qualify before signing up.

A1 course detail. Who it's for, what you'll learn, format, and pricing — all on one page before the enrolment button.
What the platform handles

Bilingual UI

Full English and Spanish support driven by a translation dictionary keyed on a ?lang= URL parameter. Applied consistently across every page, form label, validation message, and error state — not just marketing copy.

Student-type-aware enrolment

Argentine students (CUIT/CUIL and DNI with format validation) and international students (passport) follow different enrolment paths. Both collect a full postal address. The form adapts dynamically based on the selected student type.

Authentication and account management

Registration and login with scrypt-hashed passwords via Werkzeug. Flask-Login session management. Students can view enrolment details, edit personal information, update their address, change their password, and delete their account (GDPR-style data removal).

Admin panel

Role-protected view behind an is_admin flag. Displays all registered users, enrolments, and contact form submissions. No separate admin application — it lives within the same Flask blueprint.

Word of the Day

Daily-rotating vocabulary card driven by a date-based index against a curated word list. No additional database queries — just a deterministic calculation at render time. Exposed as a REST API endpoint for external use.

Contact form

Submissions persisted to the database so nothing is lost to email. WhatsApp CTA for direct replies. CSRF-protected, rate-limited to prevent spam.

REST API

A FastAPI service runs alongside the Flask app on port 8000, connecting to the same SQLite database via a standalone SQLAlchemy session. It provides authenticated JSON access to the same data — no duplication, no synchronisation overhead.

JWT authentication

HS256-signed tokens issued by POST /auth/login via OAuth2PasswordRequestForm. Verified on protected endpoints via a FastAPI dependency. Integrates with Swagger UI's Authorize button — no manual header manipulation needed during development.

Pydantic validation

All request bodies and response schemas are Pydantic BaseModel subclasses. Malformed requests return 422 Unprocessable Entity automatically. Login is rate-limited to 5 requests/minute; enquiry submission to 10/minute — both return 429 on breach.

Test suite

20 pytest tests using FastAPI's TestClient. Covers all endpoints, authentication flows, 404 handling, and Pydantic validation errors. Isolated via an in-memory SQLite database with dependency_overrides — the real school.db is never touched.

Security

Hardened by default

CSRF protection on all forms. Rate limiting on login, registration, and contact endpoints. Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options headers. SameSite=Lax session cookies with the Secure flag set in production.

No information leakage

Generic error messages on registration to prevent account enumeration — a failed login or duplicate registration gives the same response regardless of which condition triggered it. Duplicate enrolment guard prevents re-submission.

Tools used
Python 3
Language
Flask 3
Web framework
FastAPI
REST API
SQLite / SQLAlchemy
Database layer
Jinja2
Templates
Flask-WTF
Forms + CSRF
Flask-Login
Session management
Werkzeug
scrypt password hashing
python-jose
JWT / HS256
Pydantic v2
API validation
Flask-Limiter / slowapi
Rate limiting
pytest
20 tests, in-memory DB
Deliverables
← Back to portfolioBespoke services →