AWS Lambda container and local developer runner for synchronizing Notion tasks with Google Calendar.
The worker loads configuration from the mapping-domain contract and uses the event-ID-based synchronization algorithm described below. Validate configuration and run against test data first.
Releases: https://github.com/HUIXIN-TW/NotionSyncGCal/releases
- Loads normalized settings, Task sources, and Calendar mappings from the mapping-domain table.
- Expands one current-contract runtime setting per active Notion Task source.
- Runs the Notion/Google synchronization logic independently for each source.
- Uses the Notion
GCal Event IdandGCal Sync Timefields while resolving those properties strictly by persisted Notion property ID. - Supports multiple first-class Task sources and normalized Calendar-name → Calendar-ID mappings.
- Persists cloud sync logs in DynamoDB.
Synchronization is event-ID-based:
- Event matching uses the Notion
GCal Event Idfield. - Creating a Google event writes the provider event ID back to Notion.
- Update, delete, and move behavior uses that provider event ID.
- Timestamp comparison and
GCal Sync Timebehavior are implemented insrc/sync/sync.py. - Google → Notion and Notion → Google force modes are available.
- Default-Calendar behavior is driven by explicit
defaultCalendarNameconfiguration. - CLI date-range flags are in-memory execution overrides only and do not rewrite configuration.
- Multiple active Task sources are orchestrated by running the same sync function once per source.
Cloud execution reads the current mapping-domain records:
NOTION_SETTINGSsuppliestimeZoneandtimeCode; the Worker treatstimeZoneas temporal authority and retainstimeCodeonly as current contract/derived metadata, not as a date-specific runtime offset;- each active
NOTION_TASK_SOURCE#<sourceId>supplies one Notion database, defaults, and semantic property bindings; - each active
CALENDAR_MAPPING#<mappingId>supplies the persisted Notion Calendar select value (calendarName) and Google Calendar ID; - each source is converted into the current worker runtime setting using persisted Notion property IDs; mutable property names are not a runtime lookup fallback;
- each source is executed independently and results are aggregated at the user job boundary.
The worker requires the semantic bindings used by the existing synchronization implementation, including task/date, Calendar, location, extra info, GCal End Date, GCal Deleted?, GCal Event Id, GCal Sync Time, and GCal Icon.
Configuration fails closed when owner identity, lifecycle, required property bindings, Calendar-name uniqueness, default Calendar, or normalized record shape is invalid. Runtime property lookup uses propertyId only; there is no property-name fallback. SQS and EventBridge require UUID-scoped payloads with a non-empty uuid and fail closed on unsupported payload shapes.
The runtime uses an explicit mode switch via APP_MODE:
APP_MODE=local: uses.env.localsecrets andconfig/local.mapping-domain.jsonfor local development.APP_MODE=cloud: uses first-class mapping-domain records, UUID-keyed OAuth records, and SSM SecureString paths.
Current cloud/runtime notes:
- Cloud secret values are resolved via:
GOOGLE_CALENDAR_CLIENT_SECRET_SSM_PATHTOKEN_ENCRYPTION_KEY_SSM_PATH
- Cloud runtime should not use plaintext
GOOGLE_CALENDAR_CLIENT_SECRETor plaintextTOKEN_ENCRYPTION_KEYenv vars. - Token JSON files under
token/are not runtime inputs. - Cloud token payloads at rest in DynamoDB should remain
enc:v1:encrypted.
- Python
>=3.11(frompyproject.toml) uv- Notion account + Notion integration token
- Google account + OAuth client credentials
- AWS account only for
APP_MODE=cloud
Install dependencies:
uv syncRun tests:
uv run python -m unittest discover -s test -vRun coverage:
uv run coverage run -m unittest discover -s test -v
uv run coverage report -mCoverage enforcement is configured in .coveragerc (fail_under = 50).
No AWS dependency for runtime.
- Local configuration/credentials are read from
.env.local:NOTION_TOKENGOOGLE_CALENDAR_CLIENT_IDGOOGLE_CALENDAR_CLIENT_SECRETGOOGLE_CALENDAR_REFRESH_TOKENTOKEN_ENCRYPTION_KEYonly when local token values are stored asenc:v1:payloads
- Structured local sync config is read from:
config/local.mapping-domain.json
Requires a uuid and AWS access.
- Loads configuration and tokens from DynamoDB:
- mapping-domain table (
USER#<uuid>partition andSourceMappingsIndex) - Google OAuth token table
- Notion OAuth token table
- sync logs table
- mapping-domain table (
DYNAMODB_USER_TABLEis used only for sync-log summary persistence; it is not a configuration source.- Lambda environment includes SSM parameter paths:
GOOGLE_CALENDAR_CLIENT_SECRET_SSM_PATHTOKEN_ENCRYPTION_KEY_SSM_PATH
- Runtime resolves SSM SecureString values with decryption.
- Runtime does not use plaintext
GOOGLE_CALENDAR_CLIENT_SECRETor plaintextTOKEN_ENCRYPTION_KEYenv vars. - Runtime does not use local
token/*.jsonfiles.
Create local files from examples:
cp .env.local.example .env.local
cp config/local.mapping-domain.example.json config/local.mapping-domain.jsonRun sync locally with explicit mode:
APP_MODE=local uv run python src/main.py
APP_MODE=local uv run python src/main.py -t <goback_days> <goforward_days>
APP_MODE=local uv run python src/main.py -n <goback_days> <goforward_days>CLI date range flags (-t, -n) are runtime in-memory overrides only. They do not modify config/local.mapping-domain.json.
Generate a local GOOGLE_CALENDAR_REFRESH_TOKEN with:
uv run python scripts/generate-google-refresh-token.py --client-id <client_id> --client-secret <client_secret>Do not commit .env.local.
Required Lambda environment shape:
APP_MODE=cloud
APP_STAGE=dev
APP_REGION=ap-southeast-2
DYNAMODB_USER_TABLE=...
DYNAMODB_MAPPING_DOMAIN_TABLE=...
DYNAMODB_SYNC_LOGS_TABLE=...
DYNAMODB_GOOGLE_OAUTH_TOKEN_TABLE=...
DYNAMODB_NOTION_OAUTH_TOKEN_TABLE=...
GOOGLE_CALENDAR_CLIENT_ID=...
GOOGLE_CALENDAR_CLIENT_SECRET_SSM_PATH=/dev/notica/google_calendar_client_secret
TOKEN_ENCRYPTION_KEY_SSM_PATH=/dev/notica/token_encryption_keyIAM for Lambda execution role should include least privilege:
- DynamoDB read/write permissions for exact tables and required indexes.
- SSM permissions:
ssm:GetParameterfor runtime single-parameter secret resolution.ssm:GetParametersonly if batch secret lookup is introduced.- Permissions must be scoped to exact parameter ARNs.
kms:Decryptonly if those SecureString parameters use a customer-managed KMS key.
Avoid wildcard permissions such as ssm:*.
Detailed deployment workflow behavior is documented in docs/deployment.md.
Run local code with dev cloud configuration:
./scripts/local-run-dev-sync.sh --mode cloud --uuid <uuid>Run local-only mode:
./scripts/local-run-dev-sync.sh --mode localNotes:
- Cloud runner loads dev Lambda env configuration and resolves SSM values using your current AWS credentials.
- Local runner reads
.env.local. - Runner output is designed not to print sensitive secret values.
.
├── .coveragerc
├── .env.local.example
├── config/
│ └── local.mapping-domain.example.json
├── docs/
│ ├── deployment.md
│ └── local-dev-sync-runner.md
├── lambda_function.py
├── pyproject.toml
├── scripts/
│ ├── generate-google-refresh-token.py
│ ├── local-run-dev-sync.sh
│ └── local_invoke_sync_lambda.py
├── src/
│ ├── config/config.py
│ ├── gcal/
│ ├── notion/
│ ├── sync/sync.py
│ └── utils/
│ ├── ssm_secrets.py
│ └── token_crypto.py
└── test/
- Local secrets (
.env.local) are gitignored. config/local.mapping-domain.jsonis gitignored.token/is deprecated and ignored.- Cloud secret inputs are SSM path env vars, not plaintext secret env values.
- Cloud token payloads in DynamoDB should stay
enc:v1:encrypted at rest. - Do not log tokens or secret values.
- Dev deploy:
.github/workflows/deploy-dev-lambda.yml- Trigger: push to
dev - Runs validation (format/lint/unit tests/coverage/secret checks/workflow guardrails) before deploy
- Builds and pushes image, then updates dev Lambda
- Trigger: push to
- Release:
.github/workflows/release-semantic.yml- Trigger: push to
master - Runs validation before semantic release
- Creates Git tag and GitHub Release only
- Trigger: push to
- Production Lambda deploy workflow exists under
.github/workflows/disabled/and is currently disabled/manual.
docs/local-dev-sync-runner.md: detailed local/cloud runner behavior and troubleshootingdocs/deployment.md: CI/CD and environment-level deployment policy