Projects

Eight things I have built and shipped: four on AWS, three web apps, an automation for my HRIS work and a church website. Open any project to see how it works.

Showing all 8 projects

Showcase video

Nov 2026Cloud · Terraform

Disaster recovery through infrastructure as code: Terraform rebuilds a small business's system in AWS after a typhoon, from verified backups, in under six minutes.

  • Terraform
  • S3
  • EC2
  • VPC
  • IAM
  • Systems Manager
  • GitHub Actions

A disaster recovery proof of concept for Philippine small businesses. It protects a sample management system for a fictional farmers' cooperative in Naga City: members, inventory and a ledger in FastAPI and PostgreSQL 16 on Docker Compose. When a typhoon takes the office server offline, Terraform builds a temporary recovery site in AWS Singapore, restores a verified backup, and checks that the application works again.

Every hour the database is backed up with a snapshot-consistent pg_dump, restored into a throwaway test database and recounted before it counts as a backup. Verified copies go to an encrypted, versioned S3 bucket. In a recovery, a local job runner checks the backup and the Terraform plan, the operator approves, and the recovery host restores the data, checks its checksum, health and record counts, and reports the measured RTO and RPO before teardown.

  • Final real AWS drill: recovered in 5 min 29 s with 26 s of data loss, 10 of 10 checks passed. Targets were 30 minutes and 1 hour.
  • Standby costs cents a month: no servers run until a recovery is approved. Recorded compute spend for the whole project was about USD 0.01.
  • No SSH keys, no IAM users and no long-lived access keys: SSO for the operator, Session Manager for the host.
  • React and Vite dashboard live on GitHub Pages, with a simulated recovery walkthrough and the published drill results.

Infrastructure as code

The whole recovery site is Terraform

Nothing in AWS was clicked together. Five modules describe the recovery site, and one variable decides how much of it exists.

recovery_enabled = false

Standby. Backup bucket, IAM roles, log group and a budget alert. No servers; S3 storage is the only real cost.

recovery_enabled = true

Recovery. Adds the VPC, subnet, internet gateway, S3 endpoint, security group, EC2 host and a status alarm. Setting it back to false removes them and leaves the backups untouched.

  1. networkingVPC, one public subnet, internet gateway, free S3 gateway endpoint, and a security group with no inbound rules
  2. computet3.small on Amazon Linux 2023, IMDSv2 only, encrypted gp3, terminates on shutdown, bootstrapped from a template
  3. storagePrivate, encrypted, versioned backup bucket with lifecycle rules and a TLS-only policy
  4. identityHost role with Session Manager and read-only backups; an upload-only backup-writer role that cannot read or delete
  5. monitoringLog group with 7-day retention, host status alarm, AWS Budgets alerts at 50% and 100%
  • Remote state in S3Versioned, encrypted bucket with native S3 lockfile locking and prevent_destroy
  • Checked in CIfmt, validate, terraform test on a mocked provider, TFLint and Trivy in GitHub Actions
  • Plan guardThe job runner fails closed on an unexpected plan, and the operator types an approval before apply

terraform plan plan guard approval apply restore verify teardown

AWS services

  • Amazon S3Encrypted, versioned off-site backups and the Terraform state
  • Amazon EC2Temporary recovery host, created only during a recovery
  • Amazon VPCRecovery network with no inbound rules and no NAT gateway
  • AWS IAMUpload-only backup writer and a read-only recovery host role
  • IAM Identity CenterSSO sign-in for the operator, no long-lived keys
  • Systems ManagerSession Manager access and recovery progress, no SSH
  • CloudWatchRecovery logs and the host status alarm
  • AWS BudgetsSpending alerts so a forgotten drill cannot run up a bill

Recovery results

Measured recovery time and data loss
RunTime to recover (RTO)Data loss (RPO)
Final disaster drill5 min 29 s26 s
First recovery drill7 min 15 s19 min 38 s
Project target30 min60 min

Single test runs on a small synthetic dataset, not production guarantees. Off-site uploads need an operator session, so real data loss depends on the latest uploaded backup.

  • Terraform
  • AWS
  • GitHub Actions
  • Python
  • FastAPI
  • PostgreSQL 16
  • Docker Compose
  • React
  • TypeScript
  • Vite
Live demo GitHub Documentation
Showcase video

Nov 2026Web app · Open source

Turns Microsoft Project schedules into presentation-ready Gantt charts, with critical path analysis and delay simulation that shows what a slipped task does to Go-Live.

  • React
  • TypeScript
  • SVG Gantt
  • Node.js
  • MPXJ
  • Playwright

Microsoft Project is good for planning, but hard to explain in a meeting. Schedrift answers the question stakeholders actually ask: if this task slips, what happens to Go-Live? It imports .mpp, .mpx and Microsoft Project XML files and shows the whole plan in one presentation-ready view, without changing the original file.

Select one or more tasks, change their start or duration in working days, and the timeline and Go-Live variance update, along with the dependency chain driving the change. Dependencies are imported as defined (FS, SS, FF and SF, with lags), and a delay only reaches a successor when it uses up the gap already between them. Scenarios stay separate from the approved schedule. The showcase video above walks through it with a synthetic ERP rollout.

  • Executive and Detailed Gantt views, critical path focus and total float, on a custom SVG renderer with row virtualization.
  • Present mode for fullscreen meetings, baseline comparison, and Excel or CSV updates that keep the imported dependencies.
  • Local-first: a Node server bound to 127.0.0.1, no cloud, no login and no telemetry. .mpp files are parsed by Java and MPXJ from temporary copies and never modified.
  • The import report flags cycles, missing predecessors, invalid dates and unsupported features.
  • Vitest unit and integration tests and Playwright browser tests, all on synthetic schedules.
  • React
  • TypeScript
  • Zustand
  • Vite
  • Tailwind CSS
  • Node.js
  • Java
  • MPXJ
  • Vitest
  • Playwright
  • Showcase video
GitHub Design blueprint
Showcase video

LiveWeb app · AWS Amplify

My flagship web app, live on AWS: a moderated listing board where readers across the Philippines find and pass on Christian theological books.

  • Next.js
  • Amplify Gen 2
  • Cognito
  • AppSync
  • DynamoDB

What it does

Visitors browse, search and report approved listings. Members sign in with Cognito, list books with up to five photos and track review status. Moderators approve or reject with reasons; admins manage roles and read the audit log. Payment and delivery happen outside the site, by design.

How it is built

  1. Next.js App RouterAmplify Hosting, CloudFront, ACM
  2. Sign-in and rolesAmazon Cognito groups
  3. GraphQL APIAWS AppSync
  4. Private and public dataTwo DynamoDB models
  5. Moderation actionsAWS Lambda
  6. Book photosAmazon S3, pending and approved paths

Decisions that matter

  • Listings stay private until approved; only sanitised copies reach the public table.
  • The browser can never approve: Lambda owns every moderation and role change, and writes an audit entry.
  • Optimistic locking stops a moderator approving a stale revision.
  • No always-on compute and no NAT gateway, with AWS Budgets alerts on the account.
  • Next.js
  • Amplify Gen 2
  • Cognito
  • AppSync
  • DynamoDB
  • Lambda
  • S3
Read the case study Visit the live site GitHub
Showcase video

Nov 2026Web app · Open source

Free personal finance calculators built on official Philippine data: take-home pay, 13th month pay, MP2 and digital bank savings against inflation.

  • React
  • TypeScript
  • GitHub Actions
  • Playwright

"Worth it ba sa Pilipinas?" PinansyalPH is a free, open-source set of personal finance calculators built on official Philippine data. It works out take-home pay and 13th month pay, projects Pag-IBIG MP2 and digital bank savings after tax, and measures them against PSA inflation. It shows the arithmetic instead of telling people what to do with their money: no sign-up, no ads, no tracking, and inputs stay in the browser.

Every figure carries its source, reference period and retrieval date. A daily GitHub Actions workflow pulls PSA inflation and DOE fuel prices, validates them against shared zod schemas, keeps the previous file if a source fails, and commits only real changes. Bank rates and the MP2 dividend are checked by hand and never auto-published. The copy is in Taglish, and the showcase video above walks through the tools.

  • Seven tools: Sweldo, 13th Month Pay, MP2, Digital banks, Compare, Inflation and Gas.
  • Payroll follows SSS, PhilHealth, Pag-IBIG and BIR rules, versioned in one data file; 13th month pay follows PD 851 and checks the ₱90,000 tax exemption.
  • Money maths in decimal.js, rounded to centavos only where an institution would.
  • Savings shown in today's pesos; MP2 and bank products sit side by side, and nothing is ranked by headline rate.
  • 111 unit tests and 52 browser tests run before every deploy, across widths from 360px to 1920px.
  • React
  • TypeScript
  • Vite
  • Tailwind CSS
  • Recharts
  • GitHub Actions
  • Playwright
  • Showcase video
Live site GitHub

Oct 2026Cloud · Serverless

An event-driven API for HRIS support tickets: API Gateway, Lambda and DynamoDB, with a stream that emails Critical tickets through SNS.

  • API Gateway
  • Lambda
  • DynamoDB
  • SNS
  • CloudWatch

A serverless API for logging and tracking HRIS support tickets, modelled on the requests I handle at work, with made-up data. API Gateway takes the request, a Lambda function handles it and DynamoDB stores it. A DynamoDB stream feeds a second Lambda that emails me through SNS whenever a Critical ticket is created.

  • Six routes: CRUD, status and module filters, and a summary by status and module.
  • A status-index GSI turns status filters into queries instead of scans; SLA due dates are set by priority.
  • One narrow IAM role per function, no resource wildcards; API throttled to 5 requests a second.
  • JSON logs, one-week retention and a CloudWatch error alarm that emails through SNS.
  • API Gateway
  • Lambda
  • DynamoDB
  • DynamoDB Streams
  • SNS
  • CloudWatch
  • IAM
  • Python 3.12
Code and write-up on GitHub

A request goes through API Gateway to the hris-ticket-handler function, which reads or writes the HrisTickets table. Every new or changed item also lands on the table's DynamoDB stream; hris-critical-alert reads it, ignores everything except new Critical tickets, and publishes those to SNS, which emails me. CloudWatch keeps the handler's logs and alarms on its errors through a second SNS topic.

Built October 2026 in ap-southeast-1 (Singapore). All ticket data is made up.

API endpoints

Ticketing API routes
MethodRoutePurposeSuccess
POST/ticketsCreate a ticket201
GET/ticketsList tickets, with optional ?status= and ?module= filters200
GET/tickets/{id}Get one ticket200
PUT/tickets/{id}Update status, assignee, priority or resolution notes200
DELETE/tickets/{id}Delete a ticket200
GET/summaryTicket counts by status and by module200

Bad input returns 400 with a list of what is wrong, an unknown ticket returns 404, and anything unexpected returns a generic 500 while the details go to the logs. Updates and deletes use a condition check, so changing a ticket that does not exist returns 404 instead of quietly creating it.

Data model

  • One on-demand table, HrisTickets, keyed on ticketId (for example HRIS-70B503C7).
  • Fields for module, request type, priority, status, assignee and notes, with UTC timestamps; resolvedAt is set only on resolution.
  • A status-index GSI (status, then createdAt) makes status filters a query that returns the newest first, instead of a full scan.
  • SLA due dates by priority: Critical 4 hours, High 8, Medium 24, Low 72.

Security and permissions

  • hris-ticket-handler may only Put, Get, Update, Delete and Scan items on the table, and Query its index.
  • hris-critical-alert may only read the table's stream and publish to the hris-critical-alerts topic.
  • Neither policy uses a wildcard on its main resource.
  • The stage is throttled to 5 requests a second (burst 10) so a runaway script cannot run up a bill.
  • No authentication, acceptable for a short-lived test with fake data; a Cognito JWT authorizer would come first in a real deployment.

Monitoring and alerts

  • One JSON log line per request with the route and Lambda request ID; error details logged on failure.
  • One-week log retention on both functions.
  • A CloudWatch alarm on the handler's Errors metric emails me through SNS on one or more errors in five minutes; missing data counts as healthy.
  • The stream trigger uses batch size 10 from the latest record, so existing tickets never cause alerts.

Testing

Every route tested from PowerShell with Invoke-RestMethod.

Test results
TestWhat I didResult
CreatePOST a valid ticket201, ticket returned with an ID and a due date
ReadList all tickets; get one by IDStored tickets returned
Filter?status=Open and ?status=ResolvedCorrect tickets returned through the index
UpdatePUT status Resolved with notesStatus changed, resolvedAt added
SummaryGET /summaryCounts by status and by module
ValidationPOST with missing fields and a bad module400 with a list of problems
Not foundGET an ID that does not exist404
DeleteDELETE a ticket, then GET itDeleted, then 404
Critical alertCreate a Critical ticketEmail received in about a minute
Non-criticalCreate a Low ticketNo email, as intended

What went wrong

  • Wrong region. I started building in the wrong region and had to remove everything and begin again in Singapore. Almost everything in AWS is regional, so I now check the region selector before every step.
  • Empty ticket ID. Two commands pasted onto one line left the ticket ID variable empty, so GET and PUT returned a plain 500: DynamoDB rejects an empty key, and the response did not say so. The structured logs showed the cause.

Limitations and next steps

  • Add authentication, with a Cognito JWT authorizer or API keys.
  • Paginate listing; without a status filter it is a Scan capped at 50 items.
  • Handle MODIFY stream events, so raising a ticket to Critical also alerts.
  • Convert timestamps from UTC to local time in the emails.
  • Rebuild it as an AWS SAM template so it deploys with one command.
  • Add a small frontend for demos.

Cost: Lambda, API Gateway and DynamoDB on pay-per-use pricing with tiny test traffic, so the cost was negligible.

2026Cloud · Networking

Nginx on Amazon Linux 2023 inside a custom VPC: HTTP open, SSH limited to one IP, Session Manager through an IAM role, and an EBS snapshot as backup.

  • EC2
  • VPC
  • Security groups
  • IAM
  • EBS

A hands-on practice build for AWS networking and security: a static page served by Nginx on Amazon Linux 2023 inside a custom VPC (10.0.0.0/16) in Singapore. HTTP is open, SSH is limited to my IP, and Session Manager access comes from an IAM role on the instance. An EBS snapshot is the backup.

  • Root account behind MFA; daily work done as an IAM user.
  • No NAT gateway and a budget alert, to keep the bill predictable.
  • Solved: SSH "Permission denied" from a wrong key path, and Linux commands run in the wrong shell.
  • EC2
  • VPC
  • Security groups
  • IAM
  • Session Manager
  • EBS
  • Nginx
Write-up on GitHub
Secure EC2 Web Server architecture A visitor's browser reaches the internet gateway, and the public route table sends traffic to a public subnet in a custom VPC. There, an EC2 instance running Nginx sits behind a security group that allows HTTP from anywhere and SSH only from my IP. I reach the instance over SSH from my IP or through Session Manager using the instance's IAM role. An EBS snapshot backs up its volume. VPC 10.0.0.0/16 · ap-southeast-1 Public subnet, auto-assign public IPv4 Security group Visitor Browser, HTTP 80 Internet gateway Public route table 0.0.0.0/0 to the gateway EC2 · Nginx Amazon Linux 2023 EBS volume Snapshot as backup Me, admin IAM user with MFA SSH 22 from my IP, or Session Manager (IAM role)
How traffic reaches the server. No NAT gateway and no VPC endpoints, to avoid extra cost.

Why I built it

While studying for the AWS Certified Cloud Practitioner (CLF-C02) exam, I wanted to practise the services I was reading about instead of only memorising them, so I built a small but secure web server in a network I designed myself.

  • Apply EC2, VPC, security groups, IAM and EBS in one working project.
  • Follow security basics: no daily work as root, restricted access and cost controls.
  • Document the work so others can follow it.

Configuration

Architecture configuration
RegionAsia Pacific (Singapore), ap-southeast-1
VPCCustom VPC, 10.0.0.0/16, one Availability Zone
SubnetOne public subnet with auto-assign public IPv4
RoutingInternet gateway attached; route table sends 0.0.0.0/0 to it
ComputeEC2 on Amazon Linux 2023, Nginx installed by a user data script
StorageEBS volume with a snapshot as a backup
AccessSecurity group, plus an IAM role for Session Manager

How I built it

  1. Account setupPicked one region, enabled MFA on the root account, set a monthly budget with an 80% email alert, turned on Free Tier alerts, and created an IAM user with MFA in an admin group for daily work.
  2. NetworkCreated the VPC with one public subnet and no NAT gateway or VPC endpoints. Verified the internet gateway, created a public route table with a default route to it, and associated it with the subnet.
  3. ServerLaunched the instance, confirmed its status checks, and checked Nginx in the browser. Connected over SSH with a key pair, uploaded the site with scp, moved it into the Nginx web folder, and set file permissions to 644.
  4. BackupCreated an EBS snapshot of the instance volume and confirmed it reached the Completed state.

Security measures

  • Root account protected with MFA and not used for daily work.
  • Security group allows HTTP (80) from anywhere and SSH (22) only from my IP address.
  • An IAM role on the instance lets Session Manager act as a keyless backup way in.
  • No NAT gateway and a $5 budget alert as cost guardrails.
  • The private key stays off GitHub and is never shared.

Problems I solved

  • SSH "Permission denied (publickey)". The key file was not found because I ran the command from the wrong folder. The full path to the key, quoted because of spaces in the folder name, fixed it.
  • Key file permissions on Windows. The private key must be readable only by my user, so I removed inherited permissions and granted my account read access with icacls.
  • sudo error in PowerShell. I had run server commands on my own PC. Commands like sudo mv run on the server; scp runs on my computer.

Concepts it reinforced

  • Shared responsibility. AWS secures the infrastructure; I manage the OS, patches and security group rules.
  • Least privilege. Open only the ports and permissions that are needed.
  • Regions and AZs. Resources belong to a region, so staying in one avoids confusion.
  • Cost awareness. Public IPv4 addresses, NAT gateways and snapshots can all create charges.

Next steps

  • Add HTTPS with a domain name and certificate.
  • Automate the deployment with a script or infrastructure as code.
  • Add monitoring with CloudWatch.
  • Rebuild the setup with CloudFormation or Terraform.
  • Clean up: stop the instance when not in use, and delete the instance, snapshot and VPC when finished.

2026Automation · Work project

A daily Power Automate flow that replaced manual checks of about 29 HR templates across 9 business units with a Power BI completion dashboard.

  • Power Automate
  • SharePoint
  • Excel Online
  • Power BI

About 29 HR templates across 9 business units were being checked by hand. A daily Power Automate flow now scans the SharePoint library, flags files untouched for 7 days as stale, and upserts one row per file into an Excel Online table that feeds a Power BI completion dashboard.

  • Upsert keyed on each file's full path, so re-runs update rows instead of duplicating them.
  • Fixed a silent-skip bug by running the next step after the lookup succeeds or fails.
  • Completion is measured against the templates each unit actually uses; source files are only ever read.
  • Power Automate
  • SharePoint
  • Excel Online
  • Power BI
Case study on GitHub
Showcase video

2026Website · Custom domain

A six-page website for Evangelical Covenant Church Manila with its own design system, hover and scroll motion, and the SEO essentials.

  • HTML
  • CSS
  • JavaScript
  • SEO

A multi-page website for Evangelical Covenant Church Manila, a nonprofit church: ministries, background, a guide for first-time visitors, giving and contact details. I designed, built and deployed it, configured the custom domain, and added the SEO essentials.

The design starts from a small system: a gold, deep gold, soft cream and ink palette, Newsreader for headings and Figtree for body text, and content held to a 1,120px reading width so long passages stay comfortable. Small motion carries it: directory rows slide on hover with CSS transitions, and sections reveal as you scroll using an IntersectionObserver. I also cut the showcase video above, in the same type and colours as the site.

  • Six pages: Home, About, What to Expect, Ministries, Giving and Contact.
  • A first-time visitor guide that walks through the Lord's Day order of worship.
  • One responsive layout, tested on laptop, tablet and phone.
  • Hover micro-interactions and scroll reveals in plain CSS and JavaScript, no framework.
  • sitemap.xml and robots.txt for search engines, plus favicons and a web manifest.
  • HTML
  • CSS
  • JavaScript
  • Custom domain
  • SEO
  • Showcase video
ecc-manila.org GitHub

AWS services across these builds

Twenty AWS services and Terraform across four builds, each tied to what it did there. Pick a project to see its stack.

21 of 21 services and tools

"Via Amplify" marks services that Amplify Gen 2 provisions and manages for Teolohiya.site, rather than ones I configured on their own.

Compute

  • Amazon EC2Nginx web server; a temporary t3.small recovery host created by Terraform
    Secure EC2, BayanihanDR
  • AWS LambdaTicket handler and alert functions; moderation and admin actions
    Ticketing API, Teolohiya

Networking and delivery

  • Amazon VPCCustom VPCs with public subnets; a recovery network with no inbound rules and an S3 endpoint
    Secure EC2, BayanihanDR
  • Amazon API GatewayHTTP API with six routes and a 5 req/s throttle
    Ticketing API
  • Amazon CloudFrontContent delivery for the hosted Next.js site, via Amplify
    Teolohiya

Storage and databases

  • Amazon DynamoDBOn-demand table with a GSI and Streams; private and public listing models
    Ticketing API, Teolohiya
  • Amazon S3Book photos in pending and approved paths; encrypted, versioned backups and Terraform state
    Teolohiya, BayanihanDR
  • Amazon EBSSnapshot of the web server volume as a backup
    Secure EC2

Security and identity

  • AWS IAMOne least-privilege role per function; instance roles; an upload-only backup writer
    All four
  • Amazon CognitoSign-up, email sign-in, moderator and admin groups
    Teolohiya
  • AWS Certificate ManagerTLS certificate for HTTPS on the custom domain, via Amplify
    Teolohiya
  • AWS IAM Identity CenterSSO access to AWS without long-lived access keys
    Teolohiya, BayanihanDR
  • AWS Systems ManagerSession Manager access through an IAM role, with no SSH on the recovery host
    Secure EC2, BayanihanDR

Application integration

  • AWS AppSyncGraphQL API for listings and related operations
    Teolohiya
  • Amazon SNSCritical-ticket emails and alarm notifications
    Ticketing API

Monitoring and cost

  • Amazon CloudWatchStructured logs, one-week retention, an error alarm and a host status alarm
    Ticketing API, BayanihanDR
  • AWS BudgetsSpending alerts on the account, defined in Terraform for BayanihanDR
    Teolohiya, Secure EC2, BayanihanDR

Deployment and infrastructure as code

  • TerraformFive modules, S3 remote state with locking, one switch from standby to a full recovery site
    BayanihanDR
  • AWS Amplify Gen 2Hosting, GitHub-connected deploys and the backend definition
    Teolohiya
  • AWS CloudFormationDeploys the Amplify backend resources, via Amplify
    Teolohiya
  • AWS CDKDefines the backend as infrastructure as code, via Amplify
    Teolohiya