Multi-Company ERP with Google Sheets + Apps Script (100% Free) | FreeLearning365.com — Part 1 of 5

Multi-Company ERP with Google Sheets + Apps Script (100% Free) |  FreeLearning365.com — Part 1 of 5
FREELEARNING365.COM
🚀 Complete Free ERP — 5 Part Series
Google Sheets + Apps Script · POS · Inventory · Reports · AI
Follow the complete journey: Foundation → Security → POS → Intelligence → Scale
Part 1 of 5 · 100% Free Stack · New Architecture

Build a Multi-Company ERP & POS with Google Sheets + Apps Script

A complete, beginner-to-deployment tutorial — zero hosting cost, zero paid database, real business ready. New architecture: Each company gets its own Google account — perfect data isolation, zero risk of cross-tenant leaks. By the end of this 5-part series you will have a working system with login, POS, stock ledger, invoices, and multi-company support — all built on tools you already have.

Google Sheets as Database Apps Script as Backend HTML/CSS/JS as Frontend

Why This Series Exists

Most "free" business software isn't really free. It hides behind trial limits, user caps, or a ₹/$ per month paywall. Small shops in Bangladesh — a pharmacy in Mirpur, a grocery in Sylhet, a boutique in Chattogram — deserve better. They need a system that just works, on a phone, in Bangla or English, without a monthly bill.

Over the next five parts, you'll build exactly that. You will learn real-world software patterns — authentication, session tokens, concurrency locks, multi-tenant data isolation, ledgers, invoice generation — the same patterns used inside enterprise ERPs, but implemented on tools you already have.

💡
The big idea: Don't think of this as "a Google Sheet app." Think of it as a Small Business ERP Platform whose first backend happens to be Google Sheets. When your user base grows, you swap the backend for ASP.NET Core + SQL Server — without changing the UI. That's the whole point of this architecture.
01

What You Will Build (Across All 5 Parts)

Multi-Company Login

Token-based auth that isolates each shop's data completely. One login screen, many companies.

POS / Sale Screen

Search products, add to cart, apply discount, auto-calculate due, save invoice.

Stock Ledger

Never just "Stock" — a proper ledger with IN/OUT/Balance rows for auditability.

PDF Invoices

Template-driven invoices saved to Drive — no paid PDF library needed.

Dashboard & Reports

Today's sales, receivables, low stock — generated straight from your Sheets data.

AI-Ready Architecture

Structured ledger rows = perfect training data. Add a natural-language assistant later with zero refactor.

1

Why Google Sheets as a Database?

Before we write any code, it's worth answering the question every experienced developer asks first: "Aren't spreadsheets the wrong tool for this?"

The honest answer is: it depends on scale. For a small shop doing 20–200 transactions per day, a spreadsheet is not a compromise — it's actually a sensible choice. Here's why.

✅ Zero Cost

No server, no database license, no cloud bill. You need a Google account. That's it.

✅ Instant Setup

Your "database admin tool" is a spreadsheet you already know how to use. Non-technical staff can inspect data directly.

✅ Built-in Backup & Version History

Google gives you revision history for free — a feature enterprise DBAs charge thousands for.

✅ Everywhere Access

Works from any browser, any phone, any OS. No VPN, no local install.

✅ Perfect for MVP

Launch in a day, learn from real users, then decide what to upgrade. No premature engineering.

✅ Trivial Migration Path

The schema we design in Part 1 maps 1:1 onto SQL Server tables. Copy-paste your thoughts, not your data.

When Google Sheets Is the Wrong Choice

To be responsible teachers, we must tell you where the wall is. Google Sheets starts to hurt when you hit any of these:

SignalThreshold (approx.)What breaks first
Concurrent writers30+ users saving at the same momentApps Script LockService timeouts
Rows in a single sheet~50,000 – 100,000Read/write latency grows noticeably
Apps Script execution6 minutes per callHeavy report generation fails
Daily triggers90 minutes totalScheduled jobs get cut off
Companies in one Master Sheet~100 companiesMaster lookups slow down
🎯
Design rule for this series: Keep the frontend completely ignorant of where the data lives. If every backend call goes through a single apiCall() wrapper, migrating to SQL Server later becomes a one-file change — literally.
2

The New Multi-Company Architecture: One Company = One Google Account

🏗️
Architecture Update: Based on your feedback, we have redesigned the system. Instead of a Master Spreadsheet + Company Spreadsheets on a single Google account, each company gets its own completely separate Google account. This provides absolute data isolation with zero risk of cross-tenant leakage.

Why This Change Matters

In the previous design, all companies' data lived under one Google account (yours). A filter bug, a bad query, or a misconfigured permission could — in theory — expose one company's data to another. With the new architecture, that risk disappears entirely.

Here is how it works:

┌──────────────────────────────────────────────────────┐ │ FreeLearning365 ERP — Frontend UI │ │ Login · Dashboard · POS · Stock · Reports │ │ (hosted on GitHub Pages / any static host) │ └──────────────────────┬───────────────────────────────┘ │ │ Each company connects to its own │ Apps Script Web App URL │ ┌─────────────────┼─────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Company A│ │ Company B│ │ Company C│ │ Gmail │ │ Gmail │ │ Gmail │ │ Account │ │ Account │ │ Account │ │ │ │ │ │ │ │ Drive │ │ Drive │ │ Drive │ │ Sheets │ │ Sheets │ │ Sheets │ │ Apps │ │ Apps │ │ Apps │ │ Script │ │ Script │ │ Script │ └──────────┘ └──────────┘ └──────────┘ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Company │ │ Company │ │ Company │ │ A Data │ │ B Data │ │ C Data │ │ Isolated │ │ Isolated │ │ Isolated │ └──────────┘ └──────────┘ └──────────┘

How Company Routing Works

The frontend needs to know which company a user belongs to, so it can call the correct Apps Script backend. We solve this with a simple company registry — a small JSON file or a configuration object.

Company registry — how the frontend knows where to send requests
/**
 * Company Registry
 * Maps a company ID to its Apps Script Web App URL.
 * When a user logs in, we look up their CompanyId here
 * and route all API calls to the correct backend.
 */

const COMPANY_REGISTRY = {
  'C-DEMO-001': {
    name: 'Demo Store',
    backendUrl: 'https://script.google.com/macros/s/AKfycb.../exec'
  },
  'C-DEMO-002': {
    name: 'Rahman Pharmacy',
    backendUrl: 'https://script.google.com/macros/s/BKfycb.../exec'
  }
  // Add more companies here as they onboard
};

/**
 * Returns the backend URL for a given company.
 * Throws if the company is not registered.
 */
function getBackendUrl(companyId) {
  const company = COMPANY_REGISTRY[companyId];
  if (!company) {
    throw new Error('Company not registered: ' + companyId);
  }
  return company.backendUrl;
}

Advantages of the New Architecture

AspectOld ArchitectureNew Architecture
Data isolation Logical (filter by CompanyId) Physical — separate Google accounts
Security risk Code bug could leak data Zero risk — accounts cannot see each other
Storage quota Shared 15 GB across all companies 15 GB per company
Apps Script quota Shared daily limits Independent limits per company
Onboarding You create a spreadsheet per company Company owner creates their own account
Migration to SQL Migrate all at once Migrate per company independently

Trade-offs to Be Aware Of

  • You cannot run cross-company reports from a single dashboard. If you need to see "total sales across all companies," you would build a separate reporting system that pulls from each company's API.
  • Onboarding is slightly more involved — each company must set up its own Google account and Apps Script project. We provide a setup guide (this Part 1) that they follow.
  • Updating code across companies requires re-deploying each company's Apps Script. We mitigate this by keeping the backend logic identical and versioned in a shared GitHub repository.
🧠
This is how real SaaS works at the enterprise level. Salesforce, Shopify, and Google Workspace themselves use a combination of logical and physical isolation. For small businesses in Bangladesh, physical isolation via separate Google accounts is the simplest, safest, and most cost-effective approach.
3

Understanding the Google Ecosystem

Before we dive into code, let's build a mental model of every Google service you'll use. This is not trivia — understanding why each piece exists will make the rest of this series feel obvious.

Gmail

Your identity in the Google ecosystem. Every Google account starts here. The email address is your login name across all Google services.

Google Drive

Cloud file storage. 15 GB free with every account. Stores your spreadsheets, document templates, PDF invoices, and backups.

Google Docs

Word processor used as an invoice template. You design a document once with placeholders, and Apps Script fills it with real data.

Google Sheets

Spreadsheet used as your database. Each sheet (tab) becomes a table. Rows become records. Columns become fields.

Apps Script

Serverless JavaScript runtime. Your backend API. It reads/writes Sheets, generates Docs, sends Gmail, and serves HTML.

Google Cloud Console

Advanced API management. Used for OAuth consent screens, API keys, and enabling advanced services like Drive API v3.

How the Pieces Fit Together

User opens browser │ ▼ Frontend HTML/CSS/JS (hosted anywhere — GitHub Pages, Blogger, etc.) │ │ google.script.run or fetch() ▼ Apps Script Web App │ ├── reads/writes Google Sheets (your database) ├── creates/reads Google Docs (invoice templates) ├── saves PDFs to Google Drive ├── sends emails via Gmail └── uses CacheService for session tokens │ ▼ Google infrastructure (all free, all managed by Google)

What "Serverless" Really Means

When you write Apps Script, you never think about servers. There is no machine to provision, no operating system to patch, no uptime to monitor. Google runs your code when a user triggers it, then shuts it down. You pay nothing for idle time because there is no idle time.

This is the same model as AWS Lambda or Azure Functions — except Apps Script is free for consumer accounts and comes pre-authenticated with your Google identity.

Key insight: Apps Script is not a "toy" language. It is the same JavaScript engine (V8) that runs in Chrome and Node.js. Every modern JavaScript feature — let, const, arrow functions, template literals, promises, destructuring, async/await — works in Apps Script today.
4

Setting Up Your Google Account

If you already have a Gmail account, you can use it for development. But for a real business, we strongly recommend creating a dedicated Google account. Here is why and how.

Why a Dedicated Account?

  • Separation of concerns: Your personal email and your business data live in different accounts. If one is compromised, the other is safe.
  • Cleaner sharing: When you share the ERP with employees, you share the business account — not your personal one.
  • Easier transfer: If you sell the business, you transfer the Google account. No personal data leaks.
  • Professional appearance: Invoices and emails come from a business-sounding address.

Step-by-Step: Creating a Business Google Account

Open the Google Account creation page

Go to accounts.google.com/signup in your browser. You can also click "Create account" from the Gmail login page.

Choose "For work or my business"

Google will ask whether the account is for personal use or work/business. Choose For work or my business. This unlocks slightly different defaults (though the free tier is the same).

Enter your name and desired email address

Use a name that represents the business, e.g., Rahman Store and rahmanstore.erp@gmail.com. If the address is taken, try variations like rahmanstore.bd or rahmanstore2026.

Create a strong password

Use at least 12 characters with a mix of letters, numbers, and symbols. Store it in a password manager. Never reuse a password from another service.

Add a recovery phone number and email

This is critical. If you lose access to the account, recovery is your only way back in. Use a phone number you control and a secondary email address.

Enable 2-Step Verification

After creating the account, go to myaccount.google.com → Security → 2-Step Verification and turn it on. Use the Google Authenticator app or SMS. This prevents 99% of account takeover attempts.

🔐
Security is not optional. Your ERP will contain sales data, customer information, and financial records. If the Google account is compromised, everything is exposed. Enable 2FA before you do anything else.

Free Tier Limits (What You Get for Free)

ServiceFree LimitWhat It Means for Your ERP
Google Drive storage 15 GB (shared across Drive, Gmail, Photos) Enough for thousands of PDF invoices and spreadsheet data. Text data uses almost no space.
Google Sheets 10 million cells per spreadsheet Way more than a small shop will ever need. A million sales records is ~5 million cells.
Apps Script execution 6 minutes per execution Enough for any single POS transaction or report generation.
Apps Script daily trigger 90 minutes total per day Enough for scheduled reports and backups.
Gmail sending 100 recipients per day Enough for sending invoices to customers.
UrlFetchApp calls 20,000 per day Enough for any external API integration.
📊
Perspective: A small shop doing 200 sales per day will generate approximately 200 rows in the Sales sheet and 600 rows in SaleDetails (3 products per sale on average) per day. That is ~800 rows per day, ~24,000 rows per month, ~288,000 rows per year. Google Sheets handles this comfortably. Only when you approach 1 million rows in a single sheet should you consider migrating to a proper database.
5

Deep Dive: Google Apps Script

Apps Script is the engine room of your ERP. Before you write a single line, understand what it is, what it can do, and — equally important — what it cannot do.

What Is Apps Script?

Google Apps Script is a serverless JavaScript runtime that lives inside Google's infrastructure. It was originally created in 2009 as a way to automate Google Sheets. Today it is a full-fledged application platform that can:

  • Serve HTML web apps accessible from any browser
  • Read and write Google Sheets, Docs, Slides, and Forms
  • Send and receive Gmail messages
  • Create, move, and delete Google Drive files
  • Call external REST APIs via UrlFetchApp
  • Run on time-based triggers (cron jobs) or event triggers (form submissions, sheet edits)
  • Integrate with Google Cloud services like BigQuery, Vertex AI, and Maps

The V8 Runtime

Since 2020, Apps Script runs on the V8 JavaScript engine — the same engine that powers Chrome and Node.js[reference:0]. This means you get:

  • Modern syntax: let, const, arrow functions, template literals, destructuring, spread operators
  • ES6+ features: Classes, promises, generators, Map, Set, Symbol
  • Async patterns: async/await (though Apps Script services are synchronous — more on this below)
  • Better performance: V8 is significantly faster than the old Rhino runtime
⚠️
ES6 modules are not supported. You cannot use import and export statements in Apps Script. All .gs files in a project share a global scope. This is why we use plain function declarations and a flat file structure. It is a limitation, but a manageable one.

The Execution Model

Understanding how Apps Script executes is critical for writing efficient code. Here is the lifecycle of a typical request:

1. User action (clicks "Save Sale" in the UI) │ ▼ 2. google.script.run.saveSale(saleData) │ ▼ 3. Google spins up a fresh Apps Script execution (no memory of previous executions) │ ▼ 4. Your function runs · validateSession(token) · LockService.getScriptLock().waitLock(15000) · read from Sheets · write to Sheets · release lock │ ▼ 5. Return value sent back to the browser │ ▼ 6. Execution shuts down
🔄
Every execution is stateless. Global variables do not persist between calls. If you need state — like a logged-in user's session — you must store it in CacheService, PropertiesService, or a Sheet. This is why we use session tokens (Part 2).

Apps Script Quotas and Limits (2026 Reference)

Every free Google account has hard limits on Apps Script usage. Exceeding them throws errors like "Service invoked too many times" or "Exceeded maximum execution time"[reference:1]. Here is the complete reference for consumer (Gmail) accounts:

QuotaFree (Gmail) LimitMitigation Strategy
Script runtime per execution 6 minutes Process in batches; use continuation triggers for long jobs
Daily trigger runtime 90 minutes total Schedule heavy jobs at night; combine operations
UrlFetchApp calls per day 20,000 Cache external data; poll less frequently
Email recipients per day 100 Batch emails; use Google Chat for alerts
Simultaneous executions ~30 Use LockService; design for queueing
Script project size 50 MB Keep code lean; store large data in Sheets/Drive
Triggers per project 20 per user per script Audit triggers; delete orphans
CacheService entries 1,000 per script Clean expired tokens; use short TTLs
💡
Quotas reset on Pacific Time. A script that works at 11 PM local time might fail at 9 AM if the daily quota was already exhausted. Design for graceful failure: catch errors, log them, and tell the user to try again later.

Triggers: How Apps Script Runs Automatically

There are two kinds of triggers in Apps Script:

Trigger TypeHow It FiresUse Case in Our ERP
Simple triggers Automatically on edit, open, install, or form submit onOpen(e) to add a custom menu to the spreadsheet
Installable triggers Created programmatically or via the Triggers UI Time-driven trigger for daily backup; onEdit for stock alerts
Time-driven triggers Run every N minutes/hours/days Nightly report generation; low-stock email alerts

For our ERP, we will primarily use time-driven triggers for scheduled tasks and installable onEdit triggers for reactive logic. Simple triggers are useful for spreadsheet menus but cannot access external services without authorisation[reference:2].

LockService: Preventing Race Conditions

Imagine two cashiers clicking "Save Sale" at the exact same millisecond. Both scripts read the last row of the Sales sheet, both get the same invoice number, both write their rows — and now you have two different sales with the same invoice number. This is a race condition, and it is one of the most common bugs in multi-user spreadsheet apps.

LockService solves this[reference:3]. It creates a mutex (mutual exclusion) around a block of code. Only one execution can hold the lock at a time. Everyone else waits in line.

LockService pattern — every write operation in our ERP
function createSale(token, saleData) {
  const user = validateSession(token);
  const lock = LockService.getScriptLock();
  
  try {
    // Wait up to 15 seconds for other writers to finish
    lock.waitLock(15000);
    
    // === CRITICAL SECTION ===
    // Only ONE execution can be here at a time.
    // Read last invoice number, increment, write.
    // Without this lock, two cashiers could get the same number.
    
    const invoiceNo = generateInvoiceNo(companySs);
    salesSheet.appendRow([...]);
    // ... write sale details, update stock ledger ...
    
    return { success: true, invoiceNo: invoiceNo };
    
  } finally {
    // ALWAYS release the lock, even if an error occurs
    lock.releaseLock();
  }
}
🔒
Always use try/finally with locks. If you acquire a lock and then an error occurs without releasing it, every subsequent request will hang until the lock times out. The finally block guarantees the lock is released no matter what happens.

CacheService: Fast Temporary Storage

CacheService is a key-value store that lives for a short time (up to 6 hours). It is perfect for session tokens, temporary calculations, and reducing repeated Sheet reads.

CacheService pattern — session token storage
// Store a session token for 6 hours
const cache = CacheService.getScriptCache();
cache.put(token, JSON.stringify(userData), 21600);

// Retrieve it
const cached = cache.get(token);
if (cached) {
  const user = JSON.parse(cached);
  // User is authenticated
}

// Remove it on logout
cache.remove(token);

PropertiesService: Persistent Configuration

PropertiesService stores key-value pairs that persist indefinitely. Use it for configuration that should not be hardcoded: spreadsheet IDs, API keys, feature flags.

PropertiesService pattern
// In Apps Script editor: Project Settings → Script Properties
// Add: MASTER_SHEET_ID = 1aBcD...
// Add: ENCRYPTION_KEY = your-long-random-string

// Read them in code
const sheetId = PropertiesService
  .getScriptProperties()
  .getProperty('MASTER_SHEET_ID');

The Apps Script File Structure

A well-organised Apps Script project looks like this:

Project: FreeLearning365_ERP │ ├── Config.gs — Spreadsheet handles, Script Properties ├── Utils.gs — UUIDs, helpers, safe() wrapper ├── Auth.gs — login, logout, session validation ├── Pos.gs — createSale, invoice numbering ├── Stock.gs — purchases, stock adjustments ├── Reports.gs — dashboard aggregates ├── Invoice.gs — PDF generation from Docs template ├── WebApp.gs — doGet, doPost, include() │ ├── Index.html — Main UI shell ├── Login.html — Login screen ├── Dashboard.html — Dashboard page ├── POS.html — Point of Sale page ├── JS.html — Shared JavaScript ├── CSS.html — Shared CSS │ └── appsscript.json — Manifest (timezone, scopes, webapp config)

Every .gs file shares the same global scope. You can call uuid() from Auth.gs even though it is defined in Utils.gs. This is different from Node.js modules, but it is how Apps Script works.

6

Setting Up the Spreadsheet Database

In the new architecture, each company has its own Google account and therefore its own spreadsheet. There is no "Master Spreadsheet" anymore — the company spreadsheet is the database.

Step-by-Step: Creating the Company Spreadsheet

Create a new Google Sheet

Log in to the company's Google account. Go to sheets.new in your browser. Rename the spreadsheet to ERP_Database.

Create the required tabs

Rename the default tab to Products. Then add the following tabs by clicking the + button at the bottom:

  • Sales
  • SaleDetails
  • StockLedger
  • Customers
  • Suppliers
  • Purchases
  • PurchaseDetails
  • Expenses
  • Users
  • Settings

Add header rows to every sheet

Type the exact headers listed in the next section. Capitalisation matters — the code reads them by column index, but you will thank yourself later for consistency.

Freeze the header row

In every sheet: View → Freeze → 1 row. This prevents accidental data overwrites and makes long files readable.

Copy the Spreadsheet ID

From the URL docs.google.com/spreadsheets/d/THIS_PART/edit — copy the long ID between /d/ and /edit. You will paste this into Script Properties in the next section.

Sheet-by-Sheet Schema

This is the complete database schema. Every column, every data type, every purpose.

#HeaderTypeNotes
AProductIdstringUUID, generated at creation
BNamestringProduct display name
CCategorystringFree-text category
DUnitstringpcs / kg / litre
ECostPricenumberPurchase price
FSalePricenumberSelling price
GIsActivebooleanSoft-disable without deleting
HReorderLevelnumberFor low-stock alerts
ICreatedAtdatetimeWhen the product was added
JBarcodestringOptional barcode number
#HeaderTypeNotes
AInvoiceNostringe.g. INV-2026-00001
BDatedatetimeServer time of sale
CCustomerIdstring"WALK-IN" for cash sales
DSubTotalnumberBefore discount
EDiscountnumberAbsolute amount
FTotalnumberSubTotal − Discount
GPaidnumberAmount received now
HDuenumberTotal − Paid
IUserIdstringEmail of cashier
JPaymentMethodstringCash / bKash / Card
#HeaderTypeNotes
AInvoiceNostringFK to Sales
BProductIdstringFK to Products
CQtynumberSold quantity
DUnitPricenumberPrice at the time of sale
ELineTotalnumberQty × UnitPrice
FDiscountnumberPer-line discount (optional)
#HeaderTypeNotes
ADatedatetimeWhen the movement happened
BProductIdstringWhich product
CTypestringPURCHASE / SALE / ADJUST / RETURN
DRefNostringInvoice or GRN number
EQtyInnumberPositive for stock coming in
FQtyOutnumberPositive for stock going out
GBalancenumberRunning balance after this row
HNotestringReason or reference
#HeaderTypeNotes
ACustomerIdstringUUID
BNamestringFull name
CMobilestringFor SMS later
DAddressstringFree text
EOpeningBalancenumberDue carried forward
FCreatedAtdatetimeWhen the customer was added
#HeaderTypeNotes
ASupplierIdstringUUID
BNamestringSupplier name
CMobilestringContact number
DAddressstringFree text
EOpeningBalancenumberAmount payable
#HeaderTypeNotes
AGRNNostringGoods Received Note number
BDatedatetimeWhen the purchase was received
CSupplierIdstringFK to Suppliers
DTotalnumberTotal purchase amount
EPaidnumberAmount paid
FDuenumberTotal − Paid
GUserIdstringWho recorded the purchase
#HeaderTypeNotes
ADatedatetimeWhen incurred
BCategorystringRent / Electricity / Salary
CAmountnumberPositive number
DNotestringFree text
EUserIdstringWho recorded the expense
#HeaderTypeNotes
AEmailstringLogin identifier
BNamestringDisplay name
CRolestringAdmin / Cashier / Manager
DPasswordHashstringHMAC-SHA256 hex
EIsActivebooleanSuspend without deleting
FCreatedAtdatetimeAudit trail
GLastLogindatetimeLast successful login
#HeaderTypeNotes
AKeystringSetting name
BValuestringSetting value
CDescriptionstringWhat this setting does
🧠
Why UUIDs and not sequential numbers? Sequential IDs invite guessing (user #1, user #2…). UUIDs make enumeration attacks impossible and let you merge databases later without collisions. The only exception is invoice numbers, which are sequential by design (customers expect INV-2026-00001 to be followed by INV-2026-00002).

Sample Data to Insert

Before we write code, add some sample data so you can test. Copy these rows into the corresponding sheets.

Products — sample rows
ProductId | Name           | Category  | Unit | CostPrice | SalePrice | IsActive | ReorderLevel | CreatedAt          | Barcode
P001      | Rice 5kg       | Grocery   | bag  | 420       | 500       | TRUE     | 20           | 2026-09-10 10:00:00| 8901234567890
P002      | Soybean Oil 2L | Grocery   | pcs  | 290       | 350       | TRUE     | 15           | 2026-09-10 10:00:00| 8901234567891
P003      | Sugar 1kg      | Grocery   | kg   | 110       | 130       | TRUE     | 30           | 2026-09-10 10:00:00| 8901234567892
P004      | Flour 2kg      | Grocery   | bag  | 95        | 120       | TRUE     | 25           | 2026-09-10 10:00:00| 8901234567893
P005      | Lentils 1kg    | Grocery   | kg   | 130       | 160       | TRUE     | 20           | 2026-09-10 10:00:00| 8901234567894
Users — sample rows
Email                | Name        | Role    | PasswordHash | IsActive | CreatedAt          | LastLogin
owner@store.com      | Owner       | Admin   | (see Part 2) | TRUE     | 2026-09-10 10:00:00| 
cashier1@store.com   | Karim Uddin | Cashier | (see Part 2) | TRUE     | 2026-09-10 10:00:00| 
manager@store.com    | Rahim Ahmed | Manager | (see Part 2) | TRUE     | 2026-09-10 10:00:00| 

In Part 2, we will generate the actual password hashes. For now, leave the PasswordHash column empty.

7

First Code — Config.gs & Utils.gs

Now it's time to write real code. Open the Apps Script editor from your company spreadsheet: Extensions → Apps Script.

Setting Up Script Properties

Before writing code, configure your environment. In the Apps Script editor:

  1. Click the ⚙️ Project Settings icon in the left sidebar.
  2. Scroll to Script Properties.
  3. Add the following properties:
PropertyValuePurpose
SHEET_ID(paste your spreadsheet ID)Identifies the database file
ENCRYPTION_KEY(a long random string)For password hashing
COMPANY_IDC-DEMO-001Your company identifier
COMPANY_NAMEDemo StoreDisplay name
🔐
Generate a strong ENCRYPTION_KEY. Use a random string of at least 32 characters. You can generate one at random.org/strings or with a simple Apps Script function: Utilities.getUuid() + Utilities.getUuid(). Store it in a password manager. If you lose it, you cannot regenerate the same password hashes.

Config.gs — The Foundation

Open Code.gs and rename it to Config.gs. Paste the following code.

Config.gs
/**
 * Config.gs
 * Central access to script properties and spreadsheet handles.
 * Never hardcode IDs. Never hardcode keys.
 * 
 * ARCHITECTURE: One company = one Google account = one spreadsheet.
 * This file runs inside that company's Apps Script project.
 */

// --- Read from Script Properties (never hardcode these) ---
const SHEET_ID = PropertiesService
  .getScriptProperties()
  .getProperty('SHEET_ID');

const ENCRYPTION_KEY = PropertiesService
  .getScriptProperties()
  .getProperty('ENCRYPTION_KEY');

const COMPANY_ID = PropertiesService
  .getScriptProperties()
  .getProperty('COMPANY_ID');

const COMPANY_NAME = PropertiesService
  .getScriptProperties()
  .getProperty('COMPANY_NAME');

// --- Validate configuration on load ---
if (!SHEET_ID) throw new Error('SHEET_ID is not set in Script Properties.');
if (!ENCRYPTION_KEY) throw new Error('ENCRYPTION_KEY is not set in Script Properties.');

/**
 * Returns a handle to the company's spreadsheet.
 * @returns {GoogleAppsScript.Spreadsheet.Spreadsheet}
 */
function getSpreadsheet() {
  return SpreadsheetApp.openById(SHEET_ID);
}

/**
 * Returns the first sheet with the given name, or throws.
 * A tiny helper that saves a lot of null-checks.
 * @param {string} name
 * @returns {GoogleAppsScript.Spreadsheet.Sheet}
 */
function sheet(name) {
  const s = getSpreadsheet().getSheetByName(name);
  if (!s) throw new Error('Missing sheet: ' + name);
  return s;
}

/**
 * Reads all rows from a sheet as an array of objects.
 * Assumes row 1 contains headers.
 * @param {string} sheetName
 * @returns {Array<Object>}
 */
function readAll(sheetName) {
  const s = sheet(sheetName);
  const values = s.getDataRange().getValues();
  if (values.length < 2) return [];
  
  const headers = values[0];
  const rows = [];
  
  for (let i = 1; i < values.length; i++) {
    const obj = {};
    for (let j = 0; j < headers.length; j++) {
      obj[headers[j]] = values[i][j];
    }
    rows.push(obj);
  }
  return rows;
}

/**
 * Appends a single row to a sheet.
 * @param {string} sheetName
 * @param {Array} row
 */
function appendRow(sheetName, row) {
  sheet(sheetName).appendRow(row);
}

Utils.gs — Reusable Helpers

Create a new file called Utils.gs (click the + next to "Files" and choose "Script"). Paste this code.

Utils.gs
/**
 * Utils.gs
 * Reusable helpers used across the entire backend.
 */

/**
 * Generates a URL-safe unique ID.
 * @returns {string}
 */
function uuid() {
  return Utilities.getUuid().replace(/-/g, '').substring(0, 12);
}

/**
 * Writes a batch of rows starting at the bottom of a sheet.
 * Much faster than calling appendRow in a loop.
 * @param {GoogleAppsScript.Spreadsheet.Sheet} s
 * @param {Array<Array>} rows
 */
function appendRows(s, rows) {
  if (!rows || rows.length === 0) return;
  s.getRange(s.getLastRow() + 1, 1, rows.length, rows[0].length)
    .setValues(rows);
}

/**
 * Trims a string safely.
 * @param {*} v
 * @returns {string}
 */
function trim(v) {
  return (v == null ? '' : String(v)).trim();
}

/**
 * Formats a number to 2 decimals (for currency-ish output).
 * @param {number} n
 * @returns {string}
 */
function money(n) {
  return Number(n || 0).toFixed(2);
}

/**
 * Generates a sequential invoice number.
 * Reads the Sales sheet to find the last invoice number.
 * MUST be called inside a LockService lock.
 * @returns {string}
 */
function nextInvoiceNo() {
  const s = sheet('Sales');
  const lastRow = s.getLastRow();
  const year = new Date().getFullYear();
  
  if (lastRow <= 1) return 'INV-' + year + '-00001';
  
  const lastInvoice = s.getRange(lastRow, 1).getValue();
  const parts = String(lastInvoice).split('-');
  const lastNum = parseInt(parts[2], 10) || 0;
  const nextNum = (lastNum + 1).toString().padStart(5, '0');
  
  return 'INV-' + year + '-' + nextNum;
}

/**
 * Wraps a handler so the frontend always gets a consistent
 * { success, data, error } shape.
 * @param {Function} fn
 * @returns {Function}
 */
function safe(fn) {
  return function () {
    try {
      const data = fn.apply(null, arguments);
      return { success: true, data: data };
    } catch (e) {
      console.error(e);
      return { success: false, error: e.message };
    }
  };
}

/**
 * Logs an action for auditing.
 * @param {string} email
 * @param {string} action
 */
function logAction(email, action) {
  try {
    appendRow('AuditLog', [new Date(), email, action]);
  } catch (e) {
    console.error('Audit log failed:', e);
  }
}
🧩
Why safe()? Frontend code should never have to worry about whether the server threw a JavaScript Error or returned undefined. A consistent envelope — { success, data } | { success, error } — makes every .then() handler trivial to write.

Creating the AuditLog Sheet

The logAction() function writes to an AuditLog sheet. If you have not created it yet, add a new tab called AuditLog with these headers:

#HeaderPurpose
ATimestampWhen the action happened
BEmailWhich user
CActione.g. LOGIN, SALE_CREATED_INV-2026-00001
8

Testing Your Setup

Before moving on, prove that everything works. Add a temporary test function to Config.gs.

Config.gs — temporary test functions
function testConfig() {
  Logger.log('=== Configuration Test ===');
  Logger.log('Company: ' + COMPANY_NAME + ' (' + COMPANY_ID + ')');
  Logger.log('Spreadsheet: ' + getSpreadsheet().getName());
  Logger.log('Sheets found:');
  
  const sheets = getSpreadsheet().getSheets();
  sheets.forEach(function(s) {
    Logger.log('  - ' + s.getName() + ' (' + s.getLastRow() + ' rows)');
  });
}

function testReadProducts() {
  Logger.log('=== Reading Products ===');
  const products = readAll('Products');
  Logger.log('Found ' + products.length + ' products:');
  products.forEach(function(p) {
    Logger.log('  ' + p.ProductId + ': ' + p.Name + ' — ৳' + p.SalePrice);
  });
}

function testUtils() {
  Logger.log('=== Utils Test ===');
  Logger.log('UUID: ' + uuid());
  Logger.log('Money: ' + money(1234.5));
  Logger.log('Next invoice: ' + nextInvoiceNo());
  Logger.log('Trim: "' + trim('  hello  ') + '"');
}

Save the file (Ctrl+S / Cmd+S). Then:

  1. Select testConfig from the function dropdown at the top.
  2. Click Run.
  3. Google will ask for authorisation. Click Review permissions → choose your account → AdvancedGo to [project name] (unsafe)Allow. This is normal for your own scripts.
  4. Open View → Execution log.

You should see something like:

Execution log — expected output
=== Configuration Test ===
Company: Demo Store (C-DEMO-001)
Spreadsheet: ERP_Database
Sheets found:
  - Products (6 rows)
  - Sales (1 row)
  - SaleDetails (1 row)
  - StockLedger (1 row)
  - Customers (1 row)
  - Suppliers (1 row)
  - Purchases (1 row)
  - PurchaseDetails (1 row)
  - Expenses (1 row)
  - Users (4 rows)
  - Settings (1 row)
  - AuditLog (1 row)

Then run testReadProducts and testUtils. You should see your five sample products and a generated invoice number like INV-2026-00001.

If you see those lines, your foundation is solid. Every remaining part of this series will simply add new functions on top of this pattern.

Troubleshooting Common Issues

ErrorCauseFix
Missing sheet: Products Sheet name is misspelled or does not exist Check the exact tab name. Apps Script is case-sensitive.
SHEET_ID is not set Script Properties not configured Go to Project Settings → Script Properties and add SHEET_ID.
You do not have permission to call openById You are not authorised yet Run the function again and complete the authorisation flow.
Cannot read property 'length' of undefined A sheet has no headers or is empty Ensure every sheet has headers in row 1.
Exceeded maximum execution time A loop is too large or an API call is hanging Check your network; reduce batch size; add logging to find the bottleneck.
Q

Knowledge Check — Interactive Quiz

Eight quick questions. The questions and answers are stored as a JavaScript array — a pattern you will reuse later for product catalogs, FAQ schemas, and AI prompt libraries.

Part 1 Quiz

Tap an answer to check it instantly. Your score updates as you go.

Score: 0 / 0
?

Frequently Asked Questions

Is this really free, or will Google start charging me later?
Google's free tier for Sheets, Drive, and Apps Script is generous enough for small businesses. You only start paying if you upgrade to Google Workspace for domain features (custom email, more Drive storage, etc.). For the code you write in this tutorial, a personal Google account is enough to run a real shop. Each company has its own 15 GB of free storage and its own Apps Script quotas.
Do I need to know ASP.NET or SQL to complete this series?
No. This series is fully self-contained — plain JavaScript, HTML, and CSS. If you already know ASP.NET and SQL, you have a head start: you will recognise the patterns instantly, and Part 5 shows the exact migration path to that stack.
Can two cashiers use the POS at the same time?
Yes — but each write must be protected with LockService.getScriptLock(), which serialises concurrent writers. We cover this in detail in Part 3. Google allows up to ~30 simultaneous Apps Script executions, which comfortably covers small and mid-sized shops.
Where does the frontend live? Do I need a web server?
No separate server is required. Apps Script can serve HTML directly as a web app (deployed under your Google account). If you want a custom domain later, you can host the HTML on GitHub Pages and call the Apps Script API from there — both are free.
What happens when my business outgrows Google Sheets?
You swap the backend. Because the frontend talks through a single apiCall() wrapper, migrating means pointing it at a new URL — an ASP.NET Core Web API, a Node.js service, anything. Your database schema (designed in Part 1) becomes a set of SQL tables with almost no changes.
Can I use Bangla text for product names and invoices?
Absolutely. Google Sheets supports Unicode natively, and the HTML/CSS we use includes font stacks with Bengali-capable typefaces. We will show a bilingual invoice template in Part 4.
Why can't I use ES6 modules (import/export) in Apps Script?
Apps Script does not support ES6 modules. All .gs files in a project share a single global scope. This is a limitation of the platform. We work around it with plain function declarations and a flat, well-organised file structure.
How do I update the code for all companies at once?
In the new architecture, each company has its own Apps Script project. To update, you would push the same code to each project. Tools like clasp (Command Line Apps Script Projects) allow you to script this: store the code in a GitHub repository, then run a loop that deploys to each company's script ID. We cover this in Part 5.

What's Coming in Part 2

Part 1 gave you the database and the empty backend. Part 2 turns it into a living, login-protected multi-user system.

  • Password hashing with HMAC-SHA256 (no bcrypt library needed)
  • Session tokens stored in CacheService
  • The complete Auth.gs module — login(), logout(), validateSession()
  • An HTML login page wired to the backend
  • Role-based access (Admin / Cashier / Manager)
  • Audit logging on every privileged action
  • A user-management UI for shop owners to add staff
  • Deploying the web app and sharing the URL with employees
📚
Do the exercises in Part 1 before moving on. Even twenty minutes of hands-on typing will triple your retention compared to reading alone.
📖

More Free Resources on FreeLearning365

This tutorial is part of a growing library. Here are hand-picked resources that pair well with the skills you are building right now.

CAREER Job Interview Preparation — Programming, Cloud, Data, ERP & More Ace IT interviews with structured guides across programming, cloud, data engineering, ERP, SAP and more. LEARN Free Online Tutorials & Learning Paths JavaScript, Angular, Python, SQL, Data Analysis, Cloud, Software Architecture — all free. TOOLS 100+ Free Online Tools & Utilities Developer tools, SEO utilities, converters, formatters — no registration required. AI World-Class AI Prompt Generator — 40+ Professional Types Craft high-quality AI prompts for development, business writing, and analytics. TRAINING Advance Your IT Career with Professional Training Structured training paths designed for the Bangladesh IT job market. EBOOKS FreeLearning365 eBook Collection Free downloadable eBooks on programming, tools, and career development. UTILITY Free Barcode & Label Generator Perfect companion for your new POS — generate product barcodes and A4 label sheets instantly. UTILITY Free QR Code Generator Create printable QR codes for products, invoices, or store signage. BOOTSTRAP Drag & Drop Form Generator Pro — Bootstrap 5.3/4 Build clean, responsive forms fast — ideal for extending this ERP with custom data-entry screens. UTILITY Bangladesh Electricity Bill Calculator 2026 BERC tariff calculator with appliance estimator and printable report. FINANCE Bangladesh Income Tax Calculator | NBR Slabs, Rebate Calculate tax liability with current NBR slabs, rebate and minimum tax rules. FINANCE Bangladesh National Pay Scale 2026 Calculator NPS 2026 salary calculator with basic, grade pay, and allowances. AI AI Background Remover Online — Free Clean product photos for your ERP's catalog in one click. EDUCATION Bangladesh's Largest Free Question Bank — BCS, HSC, SSC, JSC, PSC Complete solutions and question banks for students across Bangladesh. EDUCATION EV Class 9-10 All Subjects Guide | SSC Notes, MCQs, CQs Complete guide for Physics, Chemistry, Biology, Math, ICT and BGS. EDUCATION HSC, SSC & Class 6-10 Lecture Sheets PDF Download 400+ completely free lecture sheets, notes and suggestions.
FREELEARNING365.COM
🚀 Complete Free ERP — 5 Part Series
Google Sheets + Apps Script · POS · Inventory · Reports · AI
Follow the complete journey: Foundation → Security → POS → Intelligence → Scale

Post a Comment

0 Comments