Technical white paper · v1.0

Timesheets: a paper time book, rebuilt inside Microsoft 365

A timesheet system that runs inside Microsoft Teams on existing Microsoft 365 licences, with Azure Functions, SharePoint and Entra ID doing the work, and no premium add-ons. How it is built, how it is secured and how it is deployed into a tenant we do not own.

Built
Written
Author
Okavyx
Status
Built
Audience
IT managers, Microsoft 365 administrators and engineers

Overview

01Executive summary

A small Australian business recorded its staff hours the way it always had: in a paper time book, filled in with a pen, then copied by hand into its payroll software each week. We replaced the book with a digital one that lives where the staff already work, inside Microsoft Teams, on the Microsoft 365 licences the business already pays for.

The system is deliberately modest in what it does and deliberately serious in how it does it. Staff record a start, a finish, a break and a note for each day. The system adds them up. Payroll reviews the week, closes it and exports a file that the payroll software imports. It applies no pay rules: that is the payroll software's job.

Underneath, it is a full Microsoft stack. Entra ID signs people in and decides what they can do. Azure Functions holds every rule. SharePoint lists hold the data, reached only through Microsoft Graph with a permission scoped to one site. Azure Static Web Apps serves the screens with a strict security policy. A Teams app opens the same screens without a second sign-in. Everything runs in the customer's own tenant and Azure subscription, and Okavyx holds no standing access to it.

0premium licences
0stored secrets in the app
6security layers
22API functions
Where it stands. Built, security-reviewed by a separate AI reviewer with no prior context, and deployed into the customer's Microsoft 365 tenant by script.

Context

02The problem and our principles

A paper time book is cheap, familiar and quietly expensive. Hours get copied twice, once by the employee and once by the bookkeeper. Nothing checks the arithmetic. A change after the fact leaves no trace. And the book lives in one place.

The obvious Microsoft answer is Power Apps. We looked at it hard and chose not to, for two reasons. Dataverse for Teams, the version included with standard licences, cannot express per-row security: "an employee may see and change only their own days, and payroll's changes are recorded" is not something its roles can say. The full Dataverse can, but needs premium licences for every user, a recurring cost out of proportion to a timesheet. And Power Apps is built in a graphical studio, which is hard to review, test and rebuild from source.

So we built on the parts of Microsoft 365 and Azure every business already has.

PrincipleWhat it means here
Replace the book, not the bookkeeperThe app records and adds up. Pay rules, overtime and categories stay in the payroll software.
Nothing the book allowed is refusedAny day, weekends included, can be recorded. Only input that is not a time is rejected.
No new licencesRuns on Microsoft 365 Business Basic and an Azure subscription costing a few dollars a month.
The customer owns itProduction lives in the customer's tenant and subscription. Our build environment is deleted at go-live.
No stored secretsNo passwords, keys or client secrets are kept in the system.
Every write is accountableWho changed what, from what to what, and when, is recorded against their Microsoft sign-in.

Design

03System architecture

The system is a single-page web app, an API and a SharePoint site, all inside the customer's Microsoft 365 tenant and Azure subscription.

CUSTOMER'S MICROSOFT 365 TENANT AND AZURE SUBSCRIPTIONStaffbrowser or TeamsStatic Web Appsign-in page, screensAzure FunctionsAPI with AuthGuardMicrosoft Graphsite-scoped identitySharePoint siteseven listsEntra IDsign-in, tokens, signing keysApplication Insightslogssign-in, tokenkey setOkavyx: no runtime access
Figure 1. Architecture. Everything sits inside the customer's tenant and subscription.

Components

ComponentMicrosoft serviceJob
IdentityMicrosoft Entra IDSign-in with the tenant's own MFA; three security groups carry the roles. One app registration exposes the API's scope.
ScreensAzure Static Web AppsServes the app with a strict Content Security Policy, HSTS and framing rules that allow Teams and nothing else.
Sign-in libraryMSAL.jsServed from the app's own origin with pinned checksums. No third-party script host at runtime.
TeamsMicrosoft Teams app (personal tab)Opens the same screens inside Teams, signed in through nested app authentication.
APIAzure Functions, Flex Consumption, PowerShell 7.622 functions holding every rule: validation, state changes, export. Scales to zero.
Data accessMicrosoft Graph with a managed identityThe API's own Azure identity, with a permission that reaches exactly one SharePoint site.
DataSharePoint Online lists (seven)Staff, pay weeks, daily entries, pay categories, public holidays, settings, audit log. Version history on every list.
Payroll hand-offSharePoint document libraryThe export file lands here for the bookkeeper to import.
TelemetryApplication InsightsErrors and timings.

Why SharePoint lists and not a database. The volume is small (around 8,000 entries a year), SharePoint is already licensed, it stays in the tenant's own data region, and it gives version history and retention for free. It has no transactions across lists, which the design handles explicitly (section 05).

Why the browser never touches SharePoint. Staff hold no SharePoint permission at all. Every read and write goes through the API, which acts as the single broker. A compromised staff account cannot go around the rules by opening the list directly, because there is nothing to open.

One codebase, any tenant. Nothing tenant-specific is in the code. Each environment is described by a config file, and the screens' settings and security headers are generated from it at deploy time. The same code runs in our build environment and the customer's production.

Behaviour

04From the book to the payroll file

OpenLockedExportedone wayReopen (until exported)Staff record and submitPayroll reviews and closesPayroll exportsOnce exported, a week cannot go back: a second export would pay the same hours twice.
Figure 2. The life of a pay week.
  1. The week opens itself. A daily timer creates the next pay week and pre-fills a blank day for every active staff member. It is safe to run twice.
  2. Staff record their days. Start, finish, break and a note, from a browser or Teams. Any day can be added, weekends included. The server works out the hours; the browser's arithmetic is never trusted.
  3. Staff submit. Their signed-in submission stands in for the signature in the book.
  4. Payroll reviews. One grid shows every person's week. Payroll can correct an entry; each change is recorded field by field against their name.
  5. Payroll closes the week. The week locks. It can be reopened until it is exported.
  6. Payroll exports. The file is built from what was recorded, checked, written to SharePoint, and the week is marked Exported. That is one way: once the payroll software has the hours, a second export would pay them twice.

A submit in detail

BrowserEntra IDFunctionsSharePoint1 ask for a token for the API scope1 token2 call the API with the token3 verify token signature, claims4 find who the caller is, from the token5 check ownership, open week, cut-off6 work out the hours on the server7 write through Graph8 record the change in the audit listresult
Figure 3. A timesheet submit, step by step.

The export is a copy of the book, not a pay calculation. One row per person per day, with the recorded hours. Every cell is quoted, and any value that would start a spreadsheet formula is neutralised, because the file will be opened in software that runs formulas.

Safety

05Getting state changes right

Small systems get concurrency wrong because they assume it will not happen. We measured instead. In an early build, two "close week" requests sent at the same moment both succeeded, and a reopen could land after an export. A week paid twice is not a theoretical bug, so the design closes that door:

  • Optimistic concurrency on every state change. Each change carries the version of the week it read (SharePoint's eTag, sent as If-Match). If someone else changed it in between, SharePoint refuses and the user is told to reload. No locks, just detection and refusal. A probe sends parallel requests and confirms the loser is always refused.
  • The export double-checks. It reads the entries, writes the file, reads them again, and only then marks the week Exported. If anything moved, the file is removed and nothing is marked.
  • Requests that change things are never blindly repeated. Reads and fixed-value updates retry on throttling and server errors. Creates retry only when Azure says the request was never processed. If a create genuinely fails mid-flight, the user is told it may have saved and to check. That is the price of never paying twice.
  • One line per person per day. Duplicates that agree are merged at export; duplicates that differ stop the export so payroll decides which is right.

Defence

06Security model

Six layers, each defending against the failure of the ones before it.

L1 Tenant boundarysingle-tenant; tokens from other tenants rejectedL2 Token validationsignature, issuer, audience, tenant, expiry checked by the APIL3 Rolesecurity groups read from the tokenL4 Site-scoped data accessone site, through the API onlyL5 Owner checkthe person comes from the token, never the requestL6 Auditwho, what, before, after, whenreads passL1 to L4every writepasses all six
Figure 4. The six security layers.
LayerMechanismDefends against
L1 Tenant boundarySingle-tenant app registration; the API rejects any token from another tenant.A valid token from someone else's Microsoft tenant.
L2 Token validationThe API verifies every token itself: signature against Microsoft's published keys, issuer, audience, tenant, expiry and not-before.Forged, expired or replayed tokens; tokens meant for another service; algorithm confusion.
L3 RoleThree Entra security groups (Employee, Payroll, Admin), read from the token. When a user is in too many groups for the token to list, the API asks Graph directly.Staff calling payroll functions.
L4 Site-scoped data accessThe managed identity holds a Graph permission limited to selected sites, plus an explicit grant on one site. Staff hold no SharePoint access.A compromised account going around the API to the data.
L5 Owner checkThe staff member is derived from the token and compared with the entry's owner.Editing someone else's timesheet by guessing an ID.
L6 AuditEvery payroll change and every state change is recorded: who (from their sign-in), what, before, after, when.Silent changes after submission.

Around the layers

  • No stored secrets. The screens are a public client; the API reaches Graph as a managed identity. There is no stored secret to leak.
  • Browser hardening. A Content Security Policy that allows scripts only from the app's own origin and network calls only to its own API, HSTS, no framing except by Teams, and no third-party script hosts.
  • Least privilege at deploy time too. The app registration staff sign in to holds only what the screens need. Building the SharePoint lists uses a separate, short-lived admin identity that is deleted the same day.
  • Fixed failure text. A rejected sign-in gets the same message whatever the reason; the detail goes to the log, not the requester.
  • Input cleaning. Notes are stripped of control, bidirectional and zero-width characters before they are stored or exported.
  • Data stays home. Lists, files and logs live in the customer's tenant and subscription.

Experience

07Teams and the browser

Staff already live in Teams, so that is where the app needs to be. A website pinned as a Teams tab is the usual shortcut, and it breaks on sign-in: a redirect inside a Teams frame does not work.

BROWSERAppMicrosoft sign-inApp, signed inredirectand backTEAMSApp in a Teams tabTeams hands over a tokensilent firstApp, signed inIf silent fails: a Teams-brokered pop-up. Never a redirect.
Figure 5. Signing in from a browser and from Teams.

The app instead uses Microsoft's nested app authentication: inside Teams, Teams itself hands the app a token for the user who is already signed in. Silent first; a Teams-brokered pop-up only if needed; never a redirect. The same code falls back to an ordinary browser sign-in outside Teams. The Teams app is a small package uploaded to the organisation's catalogue, built by a script from the same config as everything else.

The screens land by role. Someone who keeps a timesheet opens on their own week. Payroll opens on the grid of everyone's weeks. Someone with no role sees a plain "no access" card rather than an error.

Assurance

08Verification

  • A separate AI security review. A reviewer with no prior context made three parallel read-only audit passes over every function, the screens and the deploy scripts, followed by a by-hand check of the top findings. Every finding was then given a written outcome: fixed, made obsolete by a design change, or accepted with a decision on record.
  • Automated tests, each watched failing. 18 test files cover token validation with real signed tokens, roles, the hours calculation, the export file, the audit trail, retries, the Teams sign-in path and the screens. Each was broken on purpose once to prove it can fail.
  • A post-deploy probe. A read-only script runs 44 checks against a deployed environment: settings, identities, permissions, security headers and the site grant. In rehearsal it was broken deliberately and went red on exactly the two things changed.
  • A concurrency probe. Sends parallel state changes and confirms exactly one wins.
  • Supply-chain pinning. Bundled libraries carry pinned checksums, checked by a probe.
18automated test files
44post-deploy checks
2critical findings, both fixed
Critical2High6Medium13
Chart 1. Findings from the separate AI security review, by severity. Low and informational findings were also each given an outcome and are not charted.

Practice

09Deploying into a tenant we do not own

The production system belongs to the customer, in their tenant and subscription. That changes how it gets there. A single 466-line provisioning script that mixed reads and writes across Azure, Entra and SharePoint was fine for our own build environment and wrong for someone else's.

So production deployment is 15 short scripts, one job each:

11Write3Read only1Build
Chart 2. The 15 deployment scripts: 11 write, 3 only read, 1 builds the Teams package.
  • Every script that changes anything says so in its name and states at the top exactly what it touches. Reads and writes are never mixed in one script.
  • The customer's administrator runs every write. We write and rehearse the scripts; a person with authority in that tenant reads and runs them, signing in interactively. No device codes, no stored credentials.
  • Each script names the account to sign in with, immediately before the prompt, and refuses the wrong one.
  • Every object is written into the tenant build specification before it is created.
  • Rehearsed end to end in a separate copy in our own tenant first, which found and fixed six defects before the customer saw any of them.
  • The temporary build identity is deleted the same day, and the post-deploy probe confirms the result.

Okavyx holds no standing access to the production tenant after go-live.

Hindsight

10Lessons learned

  • Scope drifts towards cleverness. The first build calculated overtime bands and capped a day's hours. It rejected a real day's work. We took it all out: the app records and adds up, and the payroll software applies the rules. Less code, fewer wrong answers.
  • Platforms move under you. PowerShell 7.4 on Azure Functions reaches end of support in November 2026, and the Linux Consumption plan gets no newer version. We moved to Flex Consumption on PowerShell 7.6 before the customer's production existed, so it never has to migrate.
  • Admin consent is not an app-role assignment. Granting Graph permissions to a managed identity takes explicit role assignments, and the Function App must restart before a new role shows up in its cached token.
  • Selected-sites access is precise and unforgiving. It grants nothing until a per-site grant is made, and adding it as a delegated permission silently breaks sign-in.
  • Measure the race before you argue about it. "Two people will never close the week at once" was disproved by a probe in one run.
  • Choose tools an AI can build and a person can review. Code, config and scripts in a repository can be read, tested and rebuilt. A graphical studio cannot.

Direction

11The future

Next

Go live with all staff after the payroll preview; fix the export columns against the payroll software on site; move to the customer's own test environment and delete ours.

Medium term

Cut-off reminders as Teams messages rather than email, with any mail permission scoped to one sender mailbox; leave and public holidays as day types if payroll wants them.

Bigger ideas

The same pattern (Teams front end, Functions API, SharePoint data, managed identity, script-built per tenant) fits most small line-of-business processes still on paper: job cards, vehicle and equipment logs, leave requests, site check-ins.

Reference

12Appendix: limits and settings

ItemValue
UsersTens of staff; nothing in the design limits it to tens
VolumeAbout 8,000 daily entries a year
API runtimeAzure Functions Flex Consumption, PowerShell 7.6
API functions22 (HTTP and two timers)
Shared modules9
SharePoint lists7, version history on all
RolesEmployee, Payroll, Admin (Entra security groups)
Sign-in libraryMSAL.js, bundled with checksums
TeamsPersonal app, nested app authentication
Week statesOpen, Locked, Exported (one way)
Licences requiredMicrosoft 365 Business Basic or above; no premium
Stored secretsNone

Our work is always evolving, and so are the AI vendors and their products. Nothing described here is set in concrete. Each paper describes its system as it stood in October 2026, when it was written; the date shown is when the app was first built.

← All white papers Log a job →